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}

왜 이런짓을...?

  • 이벤트 루프를 스터디하면서 내가 이해한 것은 다음과 같다.
    1. Node.js를 실행하면 Node.js는 우선 이벤트 루프를 생성한다.
    2. Node.js는 모든 코드를 처음부터 끝까지 차례대로 실행하며, 비동기적인 작업은 이벤트 루프에 위임한다.
    3. 코드의 실행이 완료되어 콜 스택이 비게 되면 이벤트 루프에 잔여 작업이 있는지 확인한다.
    4. 이벤트 루프에 잔여 작업이 존재하는 활성 상태인 경우, 이벤트 루프가 빌 때까지 작업을 처리한 후에 앱을 종료한다.
  • 그런데 차례대로 코드를 실행하는 과정에서 async 함수를 마주치는 경우,내부의 await에서 이벤트 루프는 블로킹되는 것이 아닌가
    하는 의문이 들었다.
  • 그래서 상술한 코드도 main의 await 구문에서 2초간 대기한 후 실행될 줄 알았는데, 실행 결과는 예상과 달라 원인을 찾게 되었다.

결론

  • 단순히 async / await와 microTaskQueue에 대한 이해가 부족한 것이 원인이었다.
    • async 함수는 항상 Promise를 반환한다.
    • await 구문은 Promise의 상태가 결정될 때까지 async 함수의 실행을 일시 정지한다.
    • microTaskQueue는 이벤트 루프에 위치하는 Node.js의 기술이며, resolve된 Promise의 콜백이 삽입된다.
    • microTaskQueue는 이벤트 루프의 페이즈와 관계 없이 현재 수행 중인 작업이 종료되는 즉시 실행된다.
    • 이벤트 루프와 microTaskQueue가 관리하는 콜백들은 콜 스택이 비었을 때 실행될 수 있다.
  • 이에 따라, 상술한 코드는 다음과 같은 순서로 실행된다.
    1. Node.js가 실행되고, 우선 이벤트 루프를 생성한다.
    2. Node.js는 코드를 한 줄 한 줄 차례대로 실행한다.
    3. 이 과정에서 main의 await 구문을 마주치나, Promise의 상태가 결정되는 것을 기다리지 않고 즉시 반환해버린다.
    4. getUserAsync 실행 과정에서 이벤트 루프에 등록된 setTimeout 콜백은 2000ms 이후에서 가능한 가장 빠른 시점에 Promise를 resolve한다.
    5. 이후 console.log('before')와 bigTask(1000)가 순차 실행되지만, bigTask는 긴 시간동안 콜 스택에서 내려오지 않는다.
    6. bigTask(1000)가 종료되고 콜 스택이 빈 후에야 비로소 Node.js는 이벤트 루프에 진입하여 비동기 작업에 대한 콜백을 처리할 수 있으며, 이벤트 루프의 순서에 따라 우선 Timers 페이즈에서 setTimeout을 처리한다.
    7. setTimeout에 의해 Promise는 resolve되어 microTaskQueue에 콜백이 삽입되고, 이벤트 루프는 이를 즉시 실행한다.
    8. 일시정지되었던 프로미스가 실행을 마치게 되고, 이벤트 루프에 잔여 작업이 없으므로 앱은 종료된다.

참고

 

Node.js 이벤트 루프, 타이머, `process.nextTick()` | Node.js

Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine.

nodejs.org

 

Node.js 이벤트 루프(Event Loop) 샅샅이 분석하기

글에 들어가기에 앞서 Node.js의 이벤트 루프의 경우 공식 문서에 설명이 부족하고 이에 따라 여러 사람들이 각자 나름대로 분석한 글이 많아 무엇이 이벤트 루프의 정확한 동작인지 알기 힘듭니

www.korecmblog.com

 

async function - JavaScript | MDN

async function 선언은 AsyncFunction객체를 반환하는 하나의 비동기 함수를 정의합니다. 비동기 함수는 이벤트 루프를 통해 비동기적으로 작동하는 함수로, 암시적으로 Promise를 사용하여 결과를 반환

developer.mozilla.org

 

await - JavaScript | MDN

Promise에 의해 만족되는 값이 반환됩니다. Promise가 아닌 경우에는 그 값 자체가 반환됩니다.

developer.mozilla.org