JS Async Functionality 2 - Promise

이 시리즈는 자바스크립트에서 비동기를 다룰 때 마주치는 개념들을 다룬다. 이번 글에서는 Promise를 다룬다.

JS Async Functionality 1 - Intro


Promise:

{ pending, fulfilled, rejected }상태를 가지는 객체로, executor 함수를 인자로 받는다.

  • executor 함수( (resolve, reject ) => {} )의 역할:

    1. 비동기 함수를 호출하고
    2. 그 비동기 함수의 콜백에서 resolve를 호출한다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const fetch = url => new Promise((resolve, reject) => {
    // 이 함수가 executor 함수이다. (주석 설명 참조)

    // 1. example Async API provided by Node.js
    http.get(options, result => {
    let data;
    result.on('data', chunk => data += chunk);

    // 2. Calls either [ resolve, reject ] from async callback.
    result.on('end', () => resolve(data));
    result.on('error', err => reject(err));
    });
    });

    executor 함수는 기존의 비동기 처리 방식을 그대로 옮겨온 것으로 이해하기 어렵지 않다.

  • 다만 Promise Chaining이라는 개념으로 Callback Hell을 1차원으로 들여쓰기 단계를 낮출 수 있다.

  • 이렇게 들여쓰기 단계를 줄이는 것은 중요한데 가독성에 의한 논리 오류가 빈번하게 발생했기 때문이다.


또한 Javascript 특성상 CPS 패턴으로 작성된 비동기 처리 함수에서, 이후에 호출되는 함수는 이전 함수의 클로저 참조도 할 수 있다.

  • 부주의하게 클로저 영역의 변수들을 사용하는 경우 메모리 사용량 면에서 좋을 게 없었다.
  • (ex) 전체 비동기 절차가 끝날 때 까지 호출 함수의 지역 변수들이 해제되지 못하는 등.

Promise Chaining:

Promise는 타입이자 객체이다. Promise(); 인스턴스는 자신의 실행 흐름에 관여하는 메소드를 세 개 갖는다: then, catch, finally (참고로 메소드는 총 5개이다. race, all이 그 나머지 둘이다.)

  • Prototype에 등록된 함수로, then은 Promise를 반환하고(그래서 Chaining이 가능하고) catch는 reject된 Promise에 한 해 수행되는 조건문으로 then(undefined, onRejected)와 동등하다. finally는 JS의 try-catch-finally의 finally와 동등하다.

then

then = (onFulfilled, onRejected) => Promise

즉 then의 두 번째 인자는 catch 절인 셈이다. 웬만하면 가독성을 위해 따로 catch 절을 사용한다.

  • onFulfilled = value => Promise (여기서 value는 Promise가 resolve한 값이다. 보통 비동기 함수의 결괏값.)

  • onRejected = value => {} (여기서 value는 Promise가 reject한 값이다. 보통 Error 객체.)

  • 1
    2
    3
    4
    5
    6
    7
    p.then(onFulfilled, onRejected);

    p.then(function(value) {
    // 비동기가 별 탈 없이 진행된 경우.
    }, function(reason) {
    // 비동기 함수 수행 중 오류가 난 경우.
    });

    then에서는 값을 그냥 반환하는 경우 Promise.resolve로 감싼 것과 같다. 즉 Promise가 반환되는 것인데, 그렇기 때문에 Promise Chaining이 가능한 것이다.


catch

catch = onRejected => Promise (!)

  • catch 메소드는 try-catchcatch와 같은 역할이다. 즉, catch가 성공했느냐, 실패했느냐에 따라 다시 then이 실행될 수도 있고 다른 catch가 실행될 수도 있고 앱이 멈출 수도 있다.

  • Case 1: catch 절에서 resolved Promise를 반환하는 경우: 이후의 then 수행

  • catch 절에서 따로 throw를 하거나, Promise.reject()를 호출하지 않는 경우 Promise는 resolved 상태로 변하여 then을 수행한 것과 동등하게 된다.

  • Case 2: catch 절에서 rejected Promise를 반환하는 경우: 이후의 catch 수행

  • JS의 try-catch에서 catch는 여러 개가 존재할 수 없는 것에 비해 Promise가 rejected 상태이면 catch절은 계속해서 호출된다. 보통 여러 개의 catch 절은 특정 오류만 잡고 싶을 때 사용한다.

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var p1 = new Promise(function(resolve, reject) {
    resolve('Success');
    });

    // 아래처럼 pin-point로 catch 절을 사용하는 것이 가능하다.
    // 기존 JS에서 try-catch를 여러 번 순차적으로 사용한 것과 동등하다.

    // 1.Promise를 생성
    p1.then(function(value) { // 1. throw를 호출해 catch 절로 이동
    throw new Error('oh, no!');
    }).catch(function(e) { // 2. reject 혹은 throw를 하지 않으므로 then 수행
    console.error(e.message);
    }).then(function(){ // 3. 이 then이 수행되게 됨.
    console.log('after a catch the chain is restored');
    }).catch(function () { // 4. 만약 [2], [3]에서 throw를 하는 경우 여기로 오게 됨.
    console.log('Not fired due to the catch');
    });

finally

(설명 생략)


알기 어려운 Promise의 특징

1. then, catch는 비동기로 실행된다.

  • 아무리 Promise.resolve(); 로 resolve가 동기로 수행되더라도 then, catch는 비동기로 queue 된다.

  • 1
    2
    3
    4
    5
    6
    7
    Promise.resolve(null).then(v => console.log('Async: ' + v));
    console.log('Sync!');
    // 결과:
    // Sync!
    // Async: null
    // WHY? then이 async로 microtask queue에 들어갔기 때문.
    // then이

2: then, catch는 비동기이지만 한꺼번에 수행된다.

  • 만약 then, catch가 setTimeout과 같은 일반적인 비동기였다면 Task Queue에서 처리된다. Task Queue는 한 작업만 처리하고 나머지 작업은 다음 순서로 넘긴다.

    • 1
      2
      3
      4
      5
      6
      function loop() {
      setTimeout(loop, 0);
      }
      loop();
      // 무한 루프에 걸리지 않는다.
      // Microtask가 아니므로 이벤트 루프에서 한 작업씩만(!!!) 처리한다.
  • 그러나 Promise, then, catch는 Microtask Queue에서 수행되는데, 이는 Event Loop 내의 Event Loop으로 생각하면 된다.

    • 굳이 이렇게 하는 이유는

      1. 다른 Javascript 수행이 되지 않음을 보장
      2. 화면이 변경되지 않음을 보장 하기 위해서이다.

  • Microtask가 호출한 microtask 역시 이어서 수행되며 microtask queue가 빌 때까지 이 단계는 끝나지 않는다.

  • Promise가 연속적으로 수행되어 문제가 발생하는 예제를 생각하려고 했으나 대부분의 비동기는 microtask를 사용하지 않기에 큰 문제는 없을 것 같다. 따라서 이 본문의 내용을 몰라도 거의 문제는 없을 것 같다.


출처

Promise then

Promise catch (재밌는 점은 catch 문서는 한국어 번역이 없다는 점이다.)

Using Promises


TODO

처음에 React를 통해 ES6를 배우면서 Promise를 접했을 때보다 문서 개수나 번역이 훨씬 좋아졌다는 걸 느꼈다. 앞으로의 JS 표준을 다루는 MDN Wiki 문서가 있으면 나도 기여해야겠다

Javascript의 Generator

이 글은 자바스크립트의 제너레이터 문법에 대해 간략히 소개한다.


정의

JavaScript의 제너레이터는 function* 으로 정의된 제너레이터 함수가 반환한 객체이다. 이 객체는 이터레이터(iterator)이다.

1
2
3
4
5
6
7
8
9
function* foo() {
yield 1;
yield 2;
yield 3;
}
// foo()로 생성된 제너레이터를 순회하며 값을 읽어간다.
for (let i of foo()) {
console.log(i);
}

Generator 객체와 함수(팩토리)

제너레이터 함수를 호출하면 제너레이터 객체를 반환하고 끝난다.

제너레이터 객체의 next(...args)를 통해 제너레이터의 본문을 일정 부분 실행할 수 있다.

  • 이터레이터는 next() 함수로 파라미터를 전달할 수 없다.
  • 제너레이터가 값을 읽을 수 있기 때문에 협력적 멀티 태스킹이 가능하다.

Generator 기반 협력적 멀티 태스킹

협력적 멀티 태스킹은 코루틴에 나오는 개념이다. (추후 정리할 예정이다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go(function* producer() {
for (let i = 0; i < 10; i++) {
yield write(i);
yield sleep(100); // -- sleep이 가능해진다!
}
});
go(function* consumer() {
let v;
while (
typeof (v = yield read()) !== 'undefined'
) {
console.log('read:', v);
}
});

이 코드의 go와 같은 함수를 제너레이터 실행기라고 한다. co 라이브러리가 훌륭한 제너레이터 실행기를 제공한다.

제너레이터 실행기는 원래 동기적으로 수행되는 제너레이터를 비동기 호출을 수행하게 만든 다음 callback을 통해 다시 제너레이터를 호출하게끔 하여 비동기 코드를 동기 코드처럼 작성할 수 있게 하는 목적의 함수이다.

제너레이터 실행기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 제너레이터 실행기
function grun(g) {
const it = g();
(function iterate(val) {
const x = it.next(val);
if (!x.done) {
if (x.value instanceof Promise) {
x.value
.then(iterate)
.catch((err) => it.throw(err));
} else {
setTimeout(iterate, 0, x.value);
}
}
})();
}

// 제너레이터 실행기를 사용한 모습.
// 꽤 async-await과 같이 가독성이 있다.
function* theFutureIsNow() {
let data;
try {
data = yield Promise.all([
nfcall(fs.readFile, 'a.txt'),
nfcall(fs.readFile, 'b.txt'),
nfcall(fs.readFile, 'c.txt'),
]);
} catch (err) {
console.error(
'Unable to read one or more input files: ' +
err.message,
);
throw err;
}
yield ptimeout(60 * 1000);
try {
yield nfcall(
fs.writeFile,
'd.txt',
data[0] + data[1] + data[2],
);
} catch (err) {
console.error(
'Unable to write output file: ' +
err.message,
);
throw err;
}
}

Generator 직접 만들어보기

자료 중 재밌는 것이 있었다: Babel은 Generator를 어떻게 바꾸나. 지금 나한테는 바로 이해하긴 어렵다. 직접 만들어보면 이해에 큰 도움이 될 듯 하다.

Generator의 단점: Iterable은 가변 인자가 아니다

제너레이터 객체는 위에서 말했듯 Iterable이지만, 이는 가변 인자와는 달라서, Math.min같은 함수를 이용할 때 spead 연산자로 배열로 만들어 넘겨야 하므로, 인자 전달 부분에서 아쉽다고 한다.

1
2
3
4
5
6
7
8
9
10
11
const foo = function* () {
/* ... */
};

// 제너레이터 객체만 넘길 순 없음.
// 굳이 제너레이터 객체를 넘길거면, 받는 함수 입장에서 이터레이터를 써야 할 듯?
// for-of 문으로.
let min = Math.min(foo);

// spread 연산자로 넘겨줘야 함.
let min = Math.min(...foo());

Generator의 콜 스택?

Calling .next() method just pushes that call on the top of the stack. It will then run the code inside the generator function.

difference: it has to remember the state of all local variables, but engines already know how to do that from the implementation of closures. A generator call will restore the state so that it can continue where it left off.

일반적인 콜 스택과 동일하다고 한다. 다만 제너레이터 내에서 제너레이터를 호출하는 경우 복잡하다고 하는데, 거기까지 알 필요는 없을 듯 하다.

출처: Javascript stack model for generators | StackOverFlow

출처: Javascript Generator의 재미 (2016.12)

출처: Learning Javascript, O Reilly

TODO:

부족한 내용 보충하기. 제너레이터가 개념 뿐 아니라 사용이 중요한 개념이어서 정리가 난잡한데 다음주 중으로 정리하려고 한다.

Promise.all은 Parallel로 실행되는가?

Javascript의 모든 코드는 이벤트 루프에서 처리된다. 그리고 Node.js의 이벤트 루프 구현체로 libuv가 사용 된다는 사실은 널리 알려져 있다. 그런데 이상하지 않은가?

Promise.all은 Parallel로 실행되는가?

이벤트 루프 모델을 이해했다면 자바스크립트에 병렬 실행은 없다는 것을 이해했을 것이다. 파일, 네트워크 I/O는 자바스크립트 코드가 직접 처리하는 것이 아니고, 콜백은 이벤트 루프에 의해 호출되어 순차적으로 실행된다.

그럼, Promise.all은 어떤가?

음… 애초에 자바스크립트 코드가 병렬적으로 실행될 수 있는가?

Promise.all을 잘 몰라서 생긴 일

Promise.all은 Promise의 호출 순서와는 전혀 관계가 없다. Promise의 기본 동작을 하나도 건드리지 않는다.

Promise.all이 제공하는 기능이란, 트랜잭션과 같이 하나라도 실패하면 catch 훅으로 넘어가게 하는 것이다.

1
2
3
4
5
6
7
Promise.all([p1, p2, p3, p4, p5])
.then((values) => {
console.log(values);
})
.catch((error) => {
console.log(error.message);
});

오히려 순차적으로 Promise를 실행하는 것이 더 특별하다.

reduce를 사용해 iterable.reduce((p, fn) => p.then(fn), Promise.resolve())로 순차적으로 실행시킬 수 있다. (웬만하면 then으로 직접 잇겠지만.)

Async Functions in Javascript 1를 참고해도 좋을 것 같다.

출처: How does Promise.all all works interanlly

출처: Is Node.js native Promise.all processing in parallel or sequentially? | StackOverFlow

이벤트 루프는 환경마다 다를까?

Javascript의 모든 코드는 이벤트 루프에서 처리된다. 그리고 Node.js의 이벤트 루프 구현체로 libuv가 사용 된다는 사실은 널리 알려져 있다. 그런데 이상하지 않은가?

왜 Node.js를 설명할 때 이벤트 루프의 구현체라며 따로 소개하는 걸까?

과연 브라우저 상의 이벤트 루프와 Node.js의 이벤트 루프는 동일하다고 생각해도 될까?

이벤트 루프 구현체가 다르다는 것을 어떻게 생각하면 좋을까?


원인은 환경 차이

이벤트 루프의 처리 방식은 스펙으로 결정돼있지만 벤더마다 약간씩 다르게 구현하는 부분이 있다고 한다. Node.js 또한 예외는 아니다.

기능 브라우저 NodeJs
File, Network I/O No Yes
Event의 종류 Mouse, Keyboard Events File, Network I/O
이벤트 루프 구현체 libevent (크롬 기준) libuv
자바스크립트 엔진 V8 (크롬 기준) V8
process.nextTick (process 모듈 자체 API) No Yes
setImmediate (ECMA 표준 아님) No Yes

위 표 이외에 처리 순서의 차이가 약간 있긴 하지만 Node v11부터는 그 차이마저 적은 편이다.

결론: 환경의 차이 때문에 약간의 구현 상의 차이가 있다 정도로 받아들이자.

출처: JavaScript Event Loop vs Node JS Event Loop

Javascript의 라이센스

이 글은 자바스크립트의 라이센스 - 소유권과 결정권에 대한 내용과 근거를 찾아 정리한 글이다.


1. Javascript의 라이센스

Javascript 이해관계자는 크게 언어 명세을 결정하는 쪽과 언어를 개발하는 쪽으로 나뉜다.

주제 소유자 라이센스 생명 주기
언어 명세(ECMA 262) TC39 수정 하지 않는 선에서 사용 가능 매년
런타임(SW) 각 벤더 개별적으로 다름 상시 업데이트

재밌는 점은 TC39에 벤더들이 참여한다는 점이다.

Trademark(유사 상표권)는 Oracle에 있는데, 명칭하는 데 사용하는 것은 문제가 되지 않는다고 한다.

언어 명세(구현 코드가 아님)은 ECMA International(TC39의 상위 개념)에서 제정한다. 언어 표준의 라이센스는 이 글 | StackExchange을 참고

언어 - 인터프리터, 컴파일러, 런타임 - 구현의 몫은 각 벤더사에 있으며 벤더사가 적용하는 라이센스에 따라 각자 라이센스를 가질 수 있다.

아래는 역사를 요약 설명한 내용이다.

in 1995, Netscape decided to add a scripting language to Navigator. They pursued two routes to achieve this: collaborating with Sun Microsystems to embed the Java programming language, while also hiring Brendan Eich to embed the Scheme language.

Netscape management soon decided that the best option was for Eich to devise a new language, with syntax similar to Java and less like Scheme or other extant scripting languages. Although the new language and its interpreter implementation were officially called LiveScript when first shipped as part of a Navigator release in September 1995, the name was changed to JavaScript three months later.

1995: Netscape Navigator라는 브라우저의 기능으로 스크립트 언어를 내장하기로 개발

In November 1996, Netscape submitted JavaScript to ECMA International, as the starting point for a standard specification that all browser vendors could conform to. This led to the official release of the first ECMAScript language specification in June 1997.

1996: Netscape -> ECMA International로 소유권 이양 (이후 특정 기업의 소유는 아니게 됨)

출처: Javascript | Wikipedia

2. TC39 소개

Ecma International’s TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript.

TC39에서는 미팅 회의록을 공개하는데, 최근 미팅을 보니 주요 브라우저 벤더사만 참여하는 것은 아니고 회의마다 여러 주체나 외부 인사도 참여하는 듯 하다. TC39에서 Javascript의 스펙을 결정한다고 한다.

This Standard defines the ECMAScript 2021 general-purpose programming language.

TC39는 가장 최신의 Javascript 스펙 문서를 온라인으로 유지한다. 현재는 ECMAScript 2021이며 이 문서는 Living Standard로 주기적으로 갱신되는 듯하다.

TC39이 표준을 제정하는 방식에 대해선 ECMAScript와 TC39 | ahn.heejong을 참고하라.

3. WHATWG 소개

The WHATWG was formed in response to the slow development of World Wide Web Consortium (W3C) Web standards (…) On 28 May 2019, the W3C announced that WHATWG would be the sole publisher of the HTML and DOM standards.

WHATWG는 W3C이 제정하는 표준 중 HTML, DOM 표준을 독자적으로 제정하는 그룹이다. WHATWG는 주요 브라우저 벤더로 구성된다. 자세한 내용

WHATWG에서 제정한 HTML 5 스펙은 HTML 뿐만 아니라 - HTML5라고 해서 HTML만을 다루지는 않는다 - (현대) 웹 기술의 전반에 대해 표준화된 문서이다.

4. 번외: 구글은 왜 Chrome, V8을 오픈소스화했을까?

[Chromium Release Announcement 발췌]

With a richer set of APIs we can build more interesting apps allowing people to do more online. The more people do online, the more they can use our services. […]

We believe that open source works not only because it allows people to join us and improve our products, but also (and more importantly) because it means other projects are able to use the code we’ve developed.

[StackExchange 답변 중]

they just needed the web as a whole to become more attractive. Their decision to open-source V8 led to one such effect: the NodeJS system was built on V8 … The web has become so attractive as a development environment that it is even displacing native apps, e.g. Electron is based on Chromium.

공유되는 일부 내용을 정리해보자면, 크롬을 통해 구글은 웹 시장을 키우고 싶었고, 오픈소스화(2008)로 웹의 기능을 늘리고 늘어난 기능이 널리 퍼지는 것을 유도했다(다른 크로미움 기반 브라우저를 통해). 오픈소스를 활용해 크롬을 개발했으며, V8기반의 NodeJS(2011)를 통해 Javascript 생태계가 커졌다. (숙련된 개발자가 많아질 수 있는 환경 조성)

출처: Why did Google make Chromium Open Source? | StackExchange

TODO:

Q. Netscape은 왜 넘겼을까? 돈이 안 돼서 그런걸까? 독자적인 기능으로 탑재했다면 더 성공할 수 있지 않았을까?

Q. 굳이 오픈소스화 하지 않아도 경쟁을 통해 기능은 늘어났을 것이고 크롬 출시연도인 2009년에 이미 2위 브라우저로 시작해 2012년엔 1위 브라우저가 됐는데 오픈 소스화가 여기서 얼마나 큰 역할을 했는지는 잘 모르겠다. 좀 더 찾아봐야 할 듯.

JS Async Functionality 1 - Intro

이 글은 자바스크립트에서 비동기를 다룰 때 마주치는 개념들인 Promise, Generator, Async-Await을 큰 범위에서 다룬다. 중간 중간에 재밌는 패턴들도 수록했다.


Why Promise?

What’s Promise?
Promise는 순차적인 비동기 코드를 깔끔하게 짤 수 있게 하는 문법이다. 문법에 포함된 Promise 객체로 처리한다. Promise로 거의 모든 비동기를 처리한다고 해도 과언이 아니다.

Promise가 익숙하지 않다면 MDN을 참고:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise > https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous/Promises

비동기 작업 시 순차적인 흐름을 많이 구현해야 하는데, CPS 방식으론 간결하게 짤 수 없다. CPS 패턴 사용 시의 Tip | JSQnA 참고.

Promise의 장점: (콜백과 관련한 비교에 대한 내용은 CPS 패턴 참고.)

  • 프로미스 체인을 사용하면 작업들을 순차 실행시키는 일은 그리 어렵지 않다.
  • throw를 프로미스 체인에서 사용할 수 있다.
  • catch 될 때까지 전체 체인에 오류를 자동으로 전파할 수 있다. 비동기 오류가 누락될 확률이 줄어든다.
  • 동기적으로 값을 반환해도 비동기적인 호출을 보장한다. 함수가 동기, 비동기 반환을 섞어서 하는 것은 나쁘다.
  • Promise.all 함수를 통해 비동기 작업을 병렬로 실행할 수 있다. (이건 CPS도 가능)
  • Promise.race 함수를 통해 비동기 작업 중 가장 먼저 수행이 끝난 결과만 사용할 수 있다. (CPS에선 직접 구현해야 함.)

Promise로 함수 배열을 순차적으로 실행하는 패턴 (현재 이해 부족으로 인해 수정 필요함.):

책에 재밌는 코드가 있어 가져왔다. Promise로 함수의 배열을 순차적으로 실행하는 방법이 있을까?

1
2
3
4
5
6
7
8
9
10
11
12
13
// sequential :: Array(() => Promise) => Promise
function sequential(tasks) {
// 빈 값을 반환하는 Promise를 생성한다.
let promise = Promise.resolve();
tasks.forEach((task) => {
// promise에 then으로 체인을 걸고,
// 다음 순번의 '이전 작업'이 되기 위해 promise 변수로 할당한다.
// UPDATE: task는 Promise를 반환하는 함수여야 한다.
promise = promise.then(task);
});
// 최종 Promise를 반환한다.
return promise;
}

reduce로도 가능하다:

1
2
3
4
5
6
7
8
9
10
11
const tasks = [
/* ... */
];
let promise = tasks.reduce(
(prev, task) => prev.then(task),
Promise.resolve(),
);

promise.then((result) => {
// TODO: retreive result
});

제한된 개수로 병렬 실행하기: (현재 이해 부족으로 인해, 추후 삽입 예정)


ES8 비동기 함수

정의에 대한 자세한 내용은 MDN async function, MDN AsyncFunction 생성자를 참고하라.

ES7 비동기 함수는 비동기적으로 동작하는, async, await 문법이 활용된 함수이다.

(설명 보충 예정.)


Why Generator?

What’s Generator?

Generator는 시작 지점이 여러 개이며 중간에 실행을 정지/재개할 수 있는 함수이다.

  • 시작 지점이 여러 개: 다른 시작 지점에 대해 매번 새로운 arguments로 호출할 수 있다.
  • 정지/재개할 수 있다: 제너레이터 함수는 실행 후 값을 반환할 때 정지한다. 이후 호출하면 다시 재개된다.

Generator가 익숙하지 않다면 MDN을 참고:

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator

시작하기 전에 아래 두 코드의 결과를 모르겠다면 이후 내용을 이해하기 어려우므로, Generator에 대해 추가적으로 공부를 하기 바란다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Generator Example 1
function* fruitGenerator() {
yield 'apple';
yield 'orange';
return 'watermelon';
}

const newFruitGenerator = fruitGenerator();
console.log(newFruitGenerator.next());
console.log(newFruitGenerator.next());
console.log(newFruitGenerator.next());

// Generator Example 2
function* iteratorGenerator(arr) {
for (let i = 0; i < arr.length; i++) {
yield arr[i];
}
}

const iterator = iteratorGenerator([
'apple',
'orange',
'watermelon',
]);
let currentItem = iterator.next();
while (!currentItem.done) {
console.log(currentItem.value);
currentItem = iterator.next();
}

Generator with CPS into Async-Await:

놀랍게도 Generator에 약간의 양념을 치면 ES7 비동기 함수를 만들어낼 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const fs = require('fs');
const path = require('path');

// 제너레이터로 비동기 흐름을 구현하는 방법이다.
function asyncFlow(generatorFunction) {
// callback 함수는 비동기 함수에 CPS 패턴으로 넘겨져서, 결괏값으로 다시 제너레이터를 호출하는 데 사용된다.
function callback(err) {
if (err) {
return generator.throw(err);
}
const results = [].slice.call(arguments, 1);
generator.next(
results.length > 1 ? results : results[0],
);
}
const generator = generatorFunction(callback);
generator.next();
}

// asyncFlow, callback을 감추고, yield를 await으로 바꾼다면 async-await과 같은 문법을 지닌다.
asyncFlow(function* (callback) {
const fileName = path.basename(__filename);
const myself = yield fs.readFile(
fileName,
'utf8',
callback,
);
yield fs.writeFile(
`clone_of_${fileName}`,
myself,
callback,
);
console.log('Clone created');
});

try-catch

Async-Await과 유사하게, 제너레이터에는 throw API가 있는데, 제너레이터 함수 내에서 try-catch로 이를 처리할 수 있다:

1
2
3
const twoWay = twoWayGenerator();
twoWay.next(args); // args를 전달
twoWay.throw(new Error()); // throw로 Error 객체 전달. 제너레이터 함수 내의 catch 절로 이동하게 된다.

참고자료 전문: Async-Await ≈ Generators + Promises

참고 2: Difference between async/await and ES6 yield with generators | StackOverFlow

참고 3: ES2017 - Async vs. Yield | StackOverFlow


하나의 API로 CPS와 Promise 모두 지원하는 방법

mongoose와 같은 많은 라이브러리는 CPS와 Promise 방식을 모두 지원한다. 어떻게 한 함수로 동시에 지원할 수 있을까? 아래 코드와 같이 구현한다면 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 마지막 인자로 callback 함수를 받는다.
// Promise로 사용하는 경우 callback 함수를 넘기지 않으니, 상관 없다.
function asyncDivision(dividend, divisor, cb) {
// 항상 Promise를 반환한다.
// 어차피 CPS 패턴을 사용하는 코드라면 Promise로 결과를 받아서 처리하지 않는다.
return new Promise((resolve, reject) => {
// 비동기로 반환
process.nextTick(() => {
const result = dividend / divisor;
if (
isNaN(result) ||
!Number.isFinite(result)
) {
const error = new Error(
'Invalid operands',
);
if (cb) {
cb(error);
} // 콜백이 있으면, 콜백을 호출한다.
return reject(error); // 콜백이 있든 없든, Promise reject로 catch 체인을 실행한다.
}

if (cb) {
cb(null, result);
} // 콜백이 있으면, 콜백을 결과로 호출한다.
resolve(result); // Promise resolve로 then 체인을 실행한다.
});
});
}

장점:

  • Promise, CPS 패턴 사용자 모두에게 기능을 제공할 수 있다.

비동기와 함수형 자바스크립트

Javascript는 순수한 함수형 언어가 아니므로, 모든 코드를 함수형 패러다임을 적용해서 작성할 수 없다고 한다. 비동기를 다루는 코드에 있어서는, 특히 async-await 키워드를 사용하여 작성할 때는 명령형 코드가 되므로, 더 함수형과 멀어지게 되는데, 결론적으론 Javascript에서 함수형 패러다임을 실천할 때에는 함수형인 코드 베이스와 그렇지 않은 부분으로 나누는 게 좋다고 한다. 또한 Promise든 Async-Await이든 하나를 택해서 통일하는 게 좋다고 하니 참고 바란다.

전문: JS: Promises, async/await, and functional programming.


TODO:

  • Generator는 아직도 공부 중이다. Iterable 프로토콜에 대한 얘기도 있고, 비동기 처리 외에 Generator의 쓰임새나 Generator 자체 개념에 대해 더 공부해야 한다.
  • 코루틴에 대해서도 공부해봐야 할 것 같다. 공부 중 접하게 된 키워드이다.
  • 제너레이터에 대한 설명을 보강해야겠다.
  • 이해가 완료되면 자체 제작한 예제 코드로 교체한다.