async 와 await는 어떻게 동작할까?
내부에서 어떤 식으로 동작하는지를 파악하기 위해서 babel의 try it out에서 트랜스파일링을 해보고 파악해보자.
아래 코드를 갖고 테스트할 예정이고 비동기 함수는 사용하지 않았다.
async, await는 비동기함수만 사용하는거 아니야? 라고 생각할 수 있지만 그렇지 않다. 어떤 함수에도 await을 적용할 수 있다.
먼저 아래의 코드의 결과값을 예상해보자.
function a() {console.log('a')}
async function b() {
console.log('b1')
await a()
console.log('b2')
}
b()
console.log('c')
사실 위의 결과값을 예상할 수 있으면 아래 글을 볼 필요가 없다.
내부원리가 궁금한 분들만 보면 될 것 같다.
위의 코드를 바벨에서 돌려보면 아래와 같은 코드가 나온다.
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);
}
}
function _asyncToGenerator(fn) {
return function () {
var self = this,
args = arguments;
return new Promise(function (resolve, reject) {
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);
});
};
}
function a() {
console.log('a');
}
function b() {
return _b.apply(this, arguments);
}
function _b() {
_b = _asyncToGenerator(function* () {
console.log('b1');
yield a();
console.log('b2');
});
return _b.apply(this, arguments);
}
b();
console.log('c');
우리에게 error를 처리하는 부분까지는 볼 여유가 없다. 코드를 조금 더 간단하게 바꾼 뒤에 살펴보자.
async였던 b함수가 b와 _b로 나뉘었으며 await였던 부분이 제네레이터함수로 변경되면서 내부에서 yield로 변경된 것을 알 수 있다. 이것만 인지하고 아래로 내려가서 코드를 확인해보자.
function asyncGeneratorStep(gen, resolve, _next, key, arg) {
/**
* 제네레이터 객체의 next 메소드를 호출한다.
*/
const genValue = gen[key](arg);
/**
* generator가 done이 됐으면 resolve한다.
* 아니면 뒤의 작업은 then으로 이어서 한다. => 여기서 마이크로 테스크 큐로 들어가기 때문에 순서의 차이가 발생한다.
*/
if (genValue.done) {
resolve(genValue.value);
} else {
Promise.resolve(genValue.value).then(_next);
}
}
function _asyncToGenerator(fn) {
return function () {
const self = this;
const args = arguments;
return new Promise((resolve) => {
// gen: 제네레이터 객체
const gen = fn.apply(self, args);
const _next = (value) => {
asyncGeneratorStep(gen, resolve, _next, 'next', value);
};
// 첫번째 await까지 실행한다.
_next(undefined);
});
};
}
function a() {
console.log('a');
}
function b() {
return _b.apply(this, arguments);
}
function _b() {
function* asyncFunction() {
console.log('b1');
yield a();
console.log('b2');
}
_b = _asyncToGenerator(asyncFunction);
return _b.apply(this, arguments);
}
b();
console.log('c');
위의 코드에서 _asyncToGenerator
와 asyncGeneratorStep
를 알아보자.
_asyncToGenerator
에서 asyncGeneratorStep
를 내부적으로 사용하고있다.
function _asyncToGenerator(fn) {
return function () {
const self = this;
const args = arguments;
return new Promise((resolve) => {
const gen = fn.apply(self, args);
const _next = (value) => {
asyncGeneratorStep(gen, resolve, _next, 'next', value);
};
_next(undefined);
});
};
}
어떤 Promise객체를 반환하는 것일까?
new Promise((resolve) => {
// gen: 제네레이터 객체
const gen = fn.apply(self, args);
const _next = (value) => {
asyncGeneratorStep(gen, resolve, _next, 'next', value);
};
// 첫번째 yield(await)까지 실행한다.
_next(undefined);
});
결국 _asyncToGenerator함수는 인자로 받은 제너레이터함수를 한번 next해준다고 생각하면 된다. 즉, 첫번째 await까지 실행한다고 보면된다.
_next함수인 asyncGeneratorStep을 알아보자.
인자부터 하나씩 알아보자.
function asyncGeneratorStep(gen, resolve, _next, key, arg) {
/**
* 제네레이터 객체의 next 메소드를 호출한다.
*/
const genValue = gen[key](arg);
/**
* generator가 done이 됐으면 resolve한다.
* 아니면 뒤의 작업은 then으로 이어서 한다. => 여기서 마이크로 테스크 큐로 들어가기 때문에 순서의 차이가 발생한다.
*/
if (genValue.done) {
resolve(genValue.value);
} else {
Promise.resolve(genValue.value).then(_next);
}
}
async, awai의 원리는 여기에 숨겨있었다.
async 함수는 제너레이터 함수로 변경이되어서 실행된다.
async함수에서 변경된 제너레이터 함수는 내부적으로 yield 될 때 그 뒤의 작업은 then에게 넘겨준다.
이때 뒤의 작업은 마이크로 태스크큐에 들어가게 되고, 콜스택이 비워진 다음에 실행되기 때문에 맨 위 예제와 같은 현상이 발생하는 것이다.
_asyncToGenerator, asyncGeneratorStep 함수의 원리를 이해하고 처음 babel에서 transfile한 코드를 보게 되면 이해가 될 것이다.
결국, await의 비밀을 제너레이터와 프로미스에 있었다.
이러한 현상은 리액트에서도 예상하지 못한 결과를 가져오게 되는데 이는 다음 포스팅에서 알아보도록하자.
이해가 안가요