await
이 앞에 붙으면 어떻게 함수의 실행을 중지시킬 수 있을까?
제너레이터 함수로 변환되어 해당 작업이 끝날 때 까지 멈출 수 있다.
MDN에 async function 문서에는 다음과 같이 써있다.
async 함수에는 await식이 포함될 수 있습니다. 이 식은 async 함수의 실행을 일시 중지하고 전달 된 Promise의 해결을 기다린 다음 async 함수의 실행을 다시 시작하고 완료후 값을 반환합니다.
1. async 함수는 기본적으로 Promise
를 리턴한다.
const asyncFn = async() => {}
asyncFn() // Promise {<fulfilled>: undefined}
2. 이행된 Promise 값은 '일반적'으로 바깥으로 꺼내오지 못한다.
const result = Promise.resolve(3).then(val => val)
console.log(result) // Promise {<fulfilled>: 3}
👻 3. Promise.resolve(arg)는 arg의 값에 따라 다른 값을 리턴한다.
const result = Promise.resolve(3)
console.log(result) // Promise {<fulfilled>: 3}
// 1. pending
const result = Promise.resolve(new Promise((resolve) => setTimeout(resolve,5000)))
console.log(result) // Promise {<pending>}
// 5초 뒤
console.log(result) // Promise {<fulfilled>: undefined}
bable을 사용해서 async함수를 generator로 바꿔보자.
1. 필요한 패키지 설치
npm i @babel/cli @babel/core @babel/plugin-transform-async-to-generator -D
일반적으로
@babel/preset-env
를 주로 사용하는데, 이 async함수를_regeneratorRuntime
으로 변환해서, 이번 실험에서는 사용하지 않았다.
2. .babelrc 세팅
{
"plugins": [
"@babel/plugin-transform-async-to-generator"
]
}
3. 코드 작성
const getValue = async () => {
const value = await new Promise((resolve)=> setTimeout(resolve,5000)).then(()=> 100)
return await Promise.resolve(value).then(val => val + 100);
};
4. 코드 변환
npx babel await.js -o output.js
5. 변환 결과
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);
});
};
}
const getValue = /*#__PURE__*/ (function () {
var _ref = _asyncToGenerator(function* () {
const value = yield new Promise((resolve) =>
setTimeout(resolve, 5000)
).then(() => 100);
return Promise.resolve(value).then((val) => val + 100);
});
return function getValue() {
return _ref.apply(this, arguments);
};
})();
getValue();
const getValue = /*#__PURE__*/ (function () {
var _ref = _asyncToGenerator(function* () {
const value = yield new Promise((resolve) =>
setTimeout(resolve, 5000)
).then(() => 100);
return yield Promise.resolve(value).then((val) => val + 100);
});
return function getValue() {
return _ref.apply(this, arguments);
};
})();
getValue는 _ref 를 실행한 결과를 반환한다.
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);
});
};
}
_ref는 _asyncToGenerator의 결과이므로, Promise를 반환하는 함수이다.
따라서 getValue를 호출하는 것은 _ref를 호출하는 것이고, 즉 Promise를 반환하는 함수를 호출하는 것이다.
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);
}
}
gen[key](args)
에서 key는 next
와 throw
이다. 그럼 gen 자체에 next와 throw가 존재할까?
function* gen() {
yield 1;
yield 2;
}
const g1 = gen();
console.log(g1)
/*
[[GeneratorLocation]]: ...
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: f* gen()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
*/
g1을 콘솔로 찍어보면 알 수 있듯이, g1 자체에는 next
, throw
키워드는 존재하지 않는다.
해당 key들을 찾기 위해 Prototype 탭을 열어보자.
[[Prototype]]: Generator
> [[Prototype]]: Generator
> constructor: ...
> next: ... // 찾았다!
> return: ...
> throw: ... // 여기도 있다!
Symbol: "Generator" ...
> [[Prototype]]: Object
그렇다. prototype chaining을 통해 next와 throw를 호출하는 방식이다.
이해를 위해 큰 부분만 흐름을 정리해보았다.
1. getValue 호출
2. _next(undefined) 호출
3. 첫 번째 yield 도착
var info = gen[key](arg); // 여기서 실제로 next가 수행된다 -> gen.next(undefined)
console.log(info) // {done:false, value: Promise {<pending>}
var value = info.value; // Promise{<pending>}
아직 제너레이터 함수의 첫 번재 yield 이기 때문에 else 구문으로 진행한다.
아래 부분에서 Promise.resolve
가 즉시 fullfilled 된 promise를 반환하는 걸로 알고 있어서 이해가 불가능했었다.
하지만 윗 부분에 작성했듯이, Promise.resolve
의 인자가 promise인 경우, 해당 promise를 그대로 리턴한다.
Promise.resolve(value).then(_next,_throw)
// 첫 번째 yield 구문과 합쳐보자.
Promise.resolve(new Promise((resolve)=>setTimeout(resolve,5000)).then(()=>100)).then(_next,_throw)
5초가 지나기 전 까지 pending 상태로 있다가, 5초 후에 resolve를 호출하게 될 것이고, 뒤에 붙은 then 구문까지 처리되어 다음과 같이 변환될 것이다.
Promise.resolve(new Promise((resolve)=>setTimeout(resolve,5000)).then(()=>100)).then(_next,_throw)
// 코드가 아래 처럼 되는 건 아닌데 (나의) 이해를 돕기 위해 다음과 같이 작성해보았다.
Promise.resolve(Promise {<fulfilled>: 100}).then(_next,_throw)
위에서 설명 했듯이, Promise.resolve
의 인자가 promise인 경우 해당 promise를 그대로 리턴한다.
그러면, fullfilled된 상태의 promise에 then 메서드가 붙었으므로, _next
를 호출 하면서, _next
의 인자로 fullfilled 된 값을 같이 넘겨주게 된다.
Promise.resolve(Promise {<fulfilled>: 100}).then(_next,_throw)
// 위 코드가 결국 다음 코드를 실행시킨다.
_next(100)
어라? 제너레이터 인스턴스의 next를 호출 할 때 인자와 같이 전달하면, 해당 인자가 제너레이터 내부로 전달된다는 사실을 알고 있다.
function* () {
const value = yield new Promise((resolve) =>setTimeout(resolve, 5000)).then(() => 100);
return yield Promise.resolve(value).then((val) => val + 100);
}
아! 그래서 value에 100이라는 값을 가질 수 있구나! (제일 궁금했던 내용인 듯 😂)
그럼 또 다시 next를 호출 했으니, 위와 같은 방식을 한 번 더 반복하게 된다.
굳이 다른 점을 꼽자면, next를 한번 더 호출할 때는 info.done
값이 true인 정도?
사실 코드 자체는 길지 않은데, Promise가 어떻게 동작하는지 정확하게 알고있지 못했던 것이 시간을 많이 잡아먹는데 큰 원인이 되었다.
비록 시간은 많이 걸렸지만, 처음 async~await을 사용할 때부터 궁금했던 점이라 아주 뿌듯뽀듯 하다. (+ Promise에 대한 이해)
사실 중간에 생략된 내용도 많다. babel/preset-env
에서는 generator를 한단계 더 변환을 하는데, 그 부분은 양이 많아서 차마 엄두가 나지 않는다. 언젠간 도전해봐야지.
async/await 동작원리 (feat.제너레이터) 정말 도움을 많이 받았다. 이 글을 이해하려고 이 게시물을 썼다고 해도 무방.
충격적인 썸네일 문구에 이끌려 들어와서 잘 읽고 갑니다. 👍👍