Rethinking-Asynchronous-Javascript


자바스크립트로 비동기 작업을 하는 방법에는 여러가지가 있습니다. 이전에도 XHR, Fetch 등 몇 가지 방법으로 비동기 작업을 처리하는 방법에 대해 간단히 포스팅을 했습니다. 비동기 프로그래밍을 효율적으로 하기 위해서는 여러 다른 패턴들을 익히고 이러한 패턴들을 적절히 조합하여 읽기 쉽고 효율적인 코드를 작성하는 것입니다.

이번에는 한 가지 문제를 여러 비동기 프로그래밍 패턴을 사용해 해결하는 연습을 하고자 합니다. 한 가지 방법이 모든 면에서 좋다면 그 한가지 방법만 공부하면 되겠지만, 실상은 그렇지가 않습니다. 제네레이터를 비동기적으로 사용하려면 프로미스나 특수한 콜백과 함께 사용해야 합니다. 프라미스는 콜백에 의존합니다. 따라서 여러 비동기적 방법에 대해 익히고 장단점에 대해서도 파악해 보는것이 좋겠습니다.

차례

  • Async vs Parallel
  • Concurrency
  • Callback
  • Callback Problem
  • Thunks
  • Promises
  • Generators / Coroutines
  • Event Reactive (observables)
  • CSP (channel-oriented concurrency)

Async(비동기) vs Parallel(병렬)

자바스크립트를 접한 많은 사람들이 병렬과 비동기를 같은것으로 생각하거나 혼용하는 경우가 많습니다. 하지만, 둘 사이에 분명히 차이가 있습니다. 비동기는 '지금'과 '나중' 사이의 간극에 관한 용어이고 병렬은 동시에 일어나는 일들관 연관됩니다.

Concurrency

Callback

콜백은 간단히 말하면 어떠한 함수에 argument로 전달하는 함수로, 나중에 호출할 함수를 가리킵니다. 콜백은 자바스크립트의 오래된 비동기적 방법 중 하나입니다. 아래는 setTimeout을 사용한 콜백의 간단한 예입니다.

setTimeout(function() {
  console.log('callback!');
}, 1000);

위의 코드는 1초 뒤 console.log('callback!')이 실행되는 간단한 프로그램입니다. 하지만, 코드를 자세히 들여다 보면 프로그램을 크게 두 부분으로 나눌 수 있습니다. 한 부분은 setTimeout을 포함한 바깥 부분을, 나머지 한 부분은 callback 함수의 내부로 나눌수 있습니다. 두 부분으로 나누어진 것의 중요한 의미는 한 부분은 코드가 즉각적으로 실행이 되고, 나머지 한 부분은 지금이 아닌 이후에 실행이 된다는 것입니다.

callback으로 비동기 프로그래밍을 하고, 한 번에 여러가지를 기다려야 한다면 콜벡을 관리하기가 어려워지고 콜벡헬이라고 불리는 아래와 같은 패턴에 빠지기 쉽습니다. 함수 안에 또다른 함수가 위치하고 있고, 그 안에 또다른 콜백에 있어 코드 읽기가 쉽지 않고 그에 따라 유지 보수도 어려워 집니다.

setTimeout(function() {
  console.log('one');
  setTimeout(function() {
    console.log('two');
    setTimeout(function() {
      console.log('three');
    }, 1000);
  }, 1000);
}, 1000);

위의 코드를 개별 함수로 정리해 아래와 같이 좀 더 정돈된 형태로 코드를 작성할 수 있습니다. 겉으로 봤을때 콜벡헬로 보이지 않을 수 있지만, 자세히 들여다 보면 위의 코드의 형태와 크게 다르지 않습니다.

function one(cb) {
  console.log('one');
  setTimeout(cb, 1000);
}
function two(cb) {
  console.log('two');
  setTimeout(cb, 1000);
}
function three() {
  console.log('three');
}

one(function() {
  two(three);
});

아래의 예제는 콜백을 이용해 비동기 프로그래밍을 구현해 보는 문제입니다.

문제

  1. This exercise calls for you to write some async flow-control code. To start off with, you'll use callbacks only.
  2. Expected behavior:

    • Request all 3 files at the same time (in "parallel").
    • Render them ASAP (don't just blindly wait for all to finish loading)
    • BUT, render them in proper (obvious) order: "file1", "file2", "file3".
    • After all 3 are done, output "Complete!".

기본적으로 가상의 ajax call을 흉내내는 아래와 같은 fakeAjax 함수가 주어지고 이것을 이용해, getFile 함수를 만들고 text를 1, 2, 3 순으로 출력해야 합니다. fakeAjax 함수는 아래와 같이 작성되어져 있습니다.

function fakeAjax(url, cb) {
  var fake_responses = {
    file1: 'The first text',
    file2: 'The middle text',
    file3: 'The last text'
  };
  var randomDelay = (Math.round(Math.random() * 1e4) % 8000) + 1000;

  console.log('Requesting: ' + url);

  setTimeout(function() {
    cb(fake_responses[url]);
  }, randomDelay);
}

function output(text) {
  console.log(text);
}

그리고 작성해야 할 getFile 함수는 아래와 같습니다.

// **************************************
// The old-n-busted callback way
function getFile(file) {
  fakeAjax(file, function(text) {
    // what do we do here?
  });
}

// request all files at once in "parallel"
getFile('file1');
getFile('file2');
getFile('file3');

주의할 점은 file 1, 2, 3이 순서대로 출력되어야 하지만, file1이 먼저 들어오면 뒤의 response를 기다리지 않고 바로 출력해야 한다는 것입니다. 같은 맥락에서 file2가 들어오고 file1이 이미 출력되었다면 file3을 기다리지 않고 출력합니다.

함수를 작성하는 방법은 여러가지가 있겠지만, 위 사항을 인지하고 저는 아래와 같이 함수를 작성했습니다.

const response = {};
const alreadyPrint = ['file1', 'file2', 'file3'];

function getFile(file) {
  fakeAjax(file, function(text) {
    // what do we do here?
    response[file] = text;

    if (file === 'file1') {
      output(text);
      alreadyPrint[0] = true;
    }

    for (let i = 0; i < alreadyPrint.length; i++) {
      if (alreadyPrint[i]) {
        continue;
      }

      if (response[alreadyPrint[i]]) {
        output(text);
        alreadyPrint[i] = true;
      } else {
        break;
      }
    }
  });
}

Callback Problem

콜백을 비동기적으로 사용하게되면, 크게 두 가지 문제에 직면할 수 있습니다. 한가지는 'Inversion of Control'이고 다른 한 가지는 'Not-Reason-able'입니다.

1. Inversion of Control 'Inversion of Control'은 프로그램의 일부는 우리가 제어를 할 수 있고, 일부분은 제어를 할 수 없는 것을 의미합니다. 아래의 코드를 보면 line 1, line 2는 우리가 제어할 수 있는 영역입니다. line 3, line 4는 어느 시점에 실행을 할 지 우리가 온전히 제어를 할 수 없습니다.

// line 1
setTimeout(function() {
  // line 3
  // line 4
});
// line 2

위의 문제를 웹브라우저 API가 아닌 3rd party 라이브러리 사용하게 될 때의 예로 살펴보겠습니다. 아래와 같이 유명인사에게 매우 고가의 전자 제품을 판매하는 e-commerce 엔진을 만들어야 한다고 가정해 봅시다. 애플리케이션의 마지막 단계에서 고객이 checkout 버튼을 누르면 purchaseInfo를 비동기적으로 확인 후 결제를 진행하고 thank-you 페이지를 로딩하게 됩니다.

trackCheckout(purchaseInfo, function finish() {
  chargeCreditCard(purchaseInfo);
  showThankYouPage();
});

그러던 어느날 상품을 한 개 구매한 고객의 신용카드에 5번의 결제가 이루어 졌다고 가정해 봅시다. 프로그램의 모든 부분을 검색해 본 후, 논리적으로 판단했을 때 결제를 진행하는 trackCheckout이 콜백인 chargeCreditCard를 다섯번 호출한 경우가 유일한 문제 원인이라는 결론에 이르렀습니다.