Dev./javascript
[JS] async는 프로미스를 반환한다.
인쥭
2022. 6. 2. 11:32
반응형
- 아래와 같은 코드가 있다고 하자.
type User = {
id: number;
name: string;
age: number;
}
// 프로미스를 반환한다.
async function getUserAsync(id: number): Promise<User> {
const user: User = {
id,
name: 'ingnoh',
age: 3,
};
return new Promise((resolve => {
setTimeout(() => {
resolve(user);
}, 2000);
}));
}
(async function main() {
console.log('main start!!!'); // 첫 번째로 출력된다.
const user = await getUserAsync(1);
console.log(user); // 네 번째로 출력된다.
})();
console.log('before'); // 두 번째로 출력된다.
const before = Date.now();
bigTask(1000);
console.log(`elapsed time: ${Date.now() - before}`); // 세 번째로 출력된다.
function bigTask(taskSize: number): void {
const arr = new Array(taskSize).fill('hello');
for(let i = 0; i < 100_000; i++) {
arr.map(e => e.split('l').join(','));
}
}
- 각 코드에 대한 설명은 다음과 같다.
- getUserAsync 함수는 setTimeout에 의해 대기한 후 user 정보를 반환한다.
- main은 await getUserAsync를 통해 유저 정보를 받아온 후, 이를 콘솔에 출력한다.
- bigTask는 무의미한 동작을 수행하며, 순전히 긴 시간이 걸리는 작업을 가정하기 위해 작성한 함수이다.
- 실행 결과는 다음과 같다.
[~] ts-node async-study.ts
main start!!!
before
elapsed time: 19645
fetched user: {"id":1,"name":"ingnoh","age":3}
왜 이런짓을...?
- 이벤트 루프를 스터디하면서 내가 이해한 것은 다음과 같다.
- Node.js를 실행하면 Node.js는 우선 이벤트 루프를 생성한다.
- Node.js는 모든 코드를 처음부터 끝까지 차례대로 실행하며, 비동기적인 작업은 이벤트 루프에 위임한다.
- 코드의 실행이 완료되어 콜 스택이 비게 되면 이벤트 루프에 잔여 작업이 있는지 확인한다.
- 이벤트 루프에 잔여 작업이 존재하는 활성 상태인 경우, 이벤트 루프가 빌 때까지 작업을 처리한 후에 앱을 종료한다.
- 그런데 차례대로 코드를 실행하는 과정에서 async 함수를 마주치는 경우,내부의 await에서 이벤트 루프는 블로킹되는 것이 아닌가
하는 의문이 들었다. - 그래서 상술한 코드도 main의 await 구문에서 2초간 대기한 후 실행될 줄 알았는데, 실행 결과는 예상과 달라 원인을 찾게 되었다.
결론
- 단순히 async / await와 microTaskQueue에 대한 이해가 부족한 것이 원인이었다.
- async 함수는 항상 Promise를 반환한다.
- await 구문은 Promise의 상태가 결정될 때까지 async 함수의 실행을 일시 정지한다.
- microTaskQueue는 이벤트 루프에 위치하는 Node.js의 기술이며, resolve된 Promise의 콜백이 삽입된다.
- microTaskQueue는 이벤트 루프의 페이즈와 관계 없이 현재 수행 중인 작업이 종료되는 즉시 실행된다.
- 이벤트 루프와 microTaskQueue가 관리하는 콜백들은 콜 스택이 비었을 때 실행될 수 있다.
- 이에 따라, 상술한 코드는 다음과 같은 순서로 실행된다.
- Node.js가 실행되고, 우선 이벤트 루프를 생성한다.
- Node.js는 코드를 한 줄 한 줄 차례대로 실행한다.
- 이 과정에서 main의 await 구문을 마주치나, Promise의 상태가 결정되는 것을 기다리지 않고 즉시 반환해버린다.
- getUserAsync 실행 과정에서 이벤트 루프에 등록된 setTimeout 콜백은 2000ms 이후에서 가능한 가장 빠른 시점에 Promise를 resolve한다.
- 이후 console.log('before')와 bigTask(1000)가 순차 실행되지만, bigTask는 긴 시간동안 콜 스택에서 내려오지 않는다.
- bigTask(1000)가 종료되고 콜 스택이 빈 후에야 비로소 Node.js는 이벤트 루프에 진입하여 비동기 작업에 대한 콜백을 처리할 수 있으며, 이벤트 루프의 순서에 따라 우선 Timers 페이즈에서 setTimeout을 처리한다.
- setTimeout에 의해 Promise는 resolve되어 microTaskQueue에 콜백이 삽입되고, 이벤트 루프는 이를 즉시 실행한다.
- 일시정지되었던 프로미스가 실행을 마치게 되고, 이벤트 루프에 잔여 작업이 없으므로 앱은 종료된다.
참고