async
와await
을 사용하면 그냥await
키워드에서 기다렸다가 다음 문을 실행한다. 정도로만 알고 있었는데 콜 스택과Promise
, 마이크로 태스크 큐 관점에서 드디어 완벽히 이해할 수 있게 되어 글을 정리한다.
이 글을 읽기 전에 제너레이터가 어떻게 동작하는지 알고 읽으면 훨씬 수월할 것이다.
async
함수가 동기적으로 동작하는 모습을 확인하기 위해서는 콜 스택에 async
함수밖에 없어야 한다.이는 간단한 예제로도 알 수 있다.
async function A() {
const a = await Promise.resolve('a')
console.log(a)
return a
}
async function B() {
const b = await Promise.resolve('b')
console.log(b)
return b
}
A()
B()
console.log('c')
현재 이 코드를 실행하면 콘솔에는 어떻게 찍힐까?
아마 c
-> a
-> b
이 순서로 찍힐 것이다.(여기서 a
와 b
의 순서는 보장할 수 없다.)
당연하다. async
함수에서 기다린다라고 표현할 수 있는 행동은 async
함수 내부에서만 발생하는 일이기 때문이다.
그렇다면 이를 우리가 원하는 대로 a
-> b
-> c
이 순서대로 동작하게 하려면 어떻게 해야할까?
간단하다. 앞서 말한 전제대로 콜 스택에 async
함수만 푸시하도록 코드를 수정하면 된다.
async function A() {
const a = await Promise.resolve('a')
console.log(a)
return a
}
async function B() {
const b = await Promise.resolve('b')
console.log(b)
return b
}
async function C() {
await A()
await B()
console.log('c')
}
C()
이렇게 콜 스택에 async
함수 하나만 있다면 앞으로는 비동기를 처리해주는 브라우저와 마이크로 태스크 큐의 영역이다.
async
함수를 실행시킨다.async
함수는 제너레이터로 변환되는데 제너레이터는 실행시키면 함수 몸체를 실행하는 것이 아니라 제너레이터 객체를 반환한다.Promise
객체의 콜백함수에서 호출하여 제너레이터 객체를 다루는 로직을 작성한다./*
* gen: 제너레이터 객체
* resolve: 해당 함수를 호출한 `Promise`의 `resolve`함수
* reject: 해당 함수를 호출한 `Promise`의 `reject`함수
* _next: 제너레이터 객체의 `done`이 `true`일때까지 호출 될 `next`
* _throw: 제너레이터 객체의 `done`이 `true`일때까지 호출 될 `throw`
* key: 'next' | 'throw' (info에 할당할 메서드가 `next`인지 `throw`인지 결정
*/
function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
try {
var info = gen[key](arg);
var value = info.value;
} catch (error) {
reject(error);
return;
}
if (info.done) {
resolve(value);
} else {
Promise.resolve(value).then(_next, _throw);
}
}
/*
* fn: 제너레이터
*/
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
// gen: 제너레이터 객체
var gen = fn.apply(self, args);
function _next(value) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'next', value);
}
function _throw(err) {
asyncGeneratorStep(gen, resolve, reject, _next, _throw, 'throw', err);
}
_next(undefined);
});
};
}
value
와 done
을 사용해서 프로미스의 resolve
에 done
이 true
일때까지 계속 resolve
해주는 것이다.resolve
되고 마이크로 태스크 큐에 resolve
된 결과가 콜스택의 최상단이 된다는 것이다.이때까지 왜 async
, await
이 동기 처리 방식이 아닌 비동기 처리 방식을 동기 처럼 동작하게 한다는 것인지 이해하지 못했는데 콜스택에 async
함수만 두고 프로미스 객체를 활용하여 비동기 처리가 끝난 작업을 마이크로태스크큐에 넣으며 비어있는 콜스택에 마이크로태스크큐의 결과물이 순서대로 들어갈테니 비동기 동작을 동기처럼 동작하게 할 수 있는 것이었다.