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 자체 개념에 대해 더 공부해야 한다.
  • 코루틴에 대해서도 공부해봐야 할 것 같다. 공부 중 접하게 된 키워드이다.
  • 제너레이터에 대한 설명을 보강해야겠다.
  • 이해가 완료되면 자체 제작한 예제 코드로 교체한다.

JS Async Functionality 1 - Intro

https://jsqna.com/js-async-1/

Author

Seongbin Kim

Posted on

21-01-11

Updated on

21-01-25

Licensed under

댓글