Javascript Promise, async-await 뽀개기

seop·2022년 4월 24일
0

고찰(Consideration)

목록 보기
5/5
post-thumbnail

현재 자바스크립트의 비동기성은 이벤트루프와 Macro Task Queue, Micro Task Queue 들의 맞물림 정도만 아는 상태. 이것만 알기엔 Promise, async-await 를 100% 안다고 자신할 수 없었다.

그래서 어느정도 실험을 통해 궁금증을 해결하고 싶었고 그 속에서 내가 어느 기술을 찾아야 하는지 정리하고 싶었다. 1~2주간의 문서 공부를 통해 내가 원하는 정답은 모두 얻지는 못했다. 그렇지만 그간 진행했던 실험 결과를 바탕으로 내가 가지고 있는 지식에 기반하여 최대한 상상력을 발휘하여 내가 잘못 알고 있는 지식은 무엇인지 최대한 들춰내보는 시간을 갖고자 한다.

Promise

Promise 의 executor 는 동기적(synchronous)으로 움직인다.

아래와 같이 Promise 실행문과 일반 함수 실행문이 있다고 가정하자.

new Promise(res => { console.log('a'); res('b'); }).then(b => console.log(b));
console.log('c');

여기서 executor란, Promise 생성자에 전달되는 함수 실행문들을 의미한다. executor 는 생성자가 실행되자마자 동기적으로 실행되므로, 콘솔에 출력되는 순서는 a c b 라고 말할 수 있다. (then 의 핸들러는 Micro Task Queue에 적재되어 나중에 이벤트루프에 의해 실행된다.)

참고: Promise의 executor는 이렇게 생성하자 마자 동기적으로 eager computation 을 수행하는 반면, Observable 의 subscribe(Promise 에서는 executor)는 subscribe가 진행될 때 계산을 수행하는 lazy computation을 수행한다.

then, catch, finally 메서드들 또한 동기적으로 실행된다.

Promise.resolve('a').then(a => {console.log(a); return Promise.reject('b')}).catch(b => console.log(b));

console.log('c');

Promise 의 then, catch, finally 체인들은 Promise 가 resolved 혹은 rejected 등의 settled 상태일때 실행된다는 정도로 막연하게 생각하고 있었다. 여기서 가장 기본적이고도 중요한 사실을 간과하고 있었다. 우리가 수행하는 함수들은 모두 기본적으로 동기적으로 이루어진다는 것이었다.

객체가 생성되고 객체의 메서드가 실행되는 것은 동기적으로 실행되는 지극히 당연한 이치이다. 즉, Promise.resolve 가 실행되고 반환된 객체에서 then 을 실행하고 그 후에 catch 가 실행되고 나서야 console.log('c') 가 실행된다는 것은 지극히 당연하다는 것이다.

그렇다면 왜 출력 결과는 c a b 가 되는가? 그 이유는 then, catch, finally 메서드의 역할에 존재한다. 이 메서드들은 '구독'의 역할을 수행한다. Promise 체인들의 반환값을 console.log 로 뜯어보면 최초엔 모두 pending 상태인 Promise 객체라는 것을 알 수 있다. 구독이란 것은, 그 객체가 settled 상태가 되면 핸들러(콜백함수)를 Micro Task Queue에 적재하겠다는 의미가 된다.

setTimeout 이 섞이면 어떤 내부구조로 돌아가는가?

한번쯤 짚고넘어가야 할 문제이다. Macro Task Queue와 Micro Task Queue 가 섞여있으니까. 다음과 같은 예제를 구상해보았다.

new Promise(res => setTimeout(() => res('a'), 2000)).then(a => console.log(a));
console.log('b');

Promise의 executor 가 동기적으로 실행된다고 하지만 setTimeout(이 함수 자체도 동기적으로 실행)의 콜백이 비동기적으로 실행되는 것이므로, 'a'라는 값의 resolve는 2초후에 이루어진다. 콘솔 출력 결과는 b a.

기술적으로 자세히 말하자면 최초 Promise executor 실행 -> setTimeout 실행 -> console.log('b') 실행 -> 2초 후 setTimeout 의 콜백이 Macro Task Queue에 적재 -> Promise가 resolve 된다면 then 속의 핸들러가 Micro Task Queue에 적재 가 됩니다. 여기서 눈여겨봐야 할 것은, resolve 를 수행하는 setTimeout 콜백은 Macro Task Queue에 적재된다는 것입니다. Macro Task Queue 는 Micro Task Queue 보다 이벤트루프에 의한 실행 우선순위가 낮습니다. 만약 어딘가의 다른 로직에 의해 Micro Task Queue 에 계속 함수가 적재된다면, 우선순위에 계속 밀려 해당 예제는 늦게 resolve 될 여지가 있습니다.

async-await

async-await 는 Promise 를 완벽히 대체할 수 있다.

아래의 첫번째 예제 async-await는 두번째 예제 Promise로 나타낼 수 있다.

async function test() {
    console.log('a');

    const b = await Promise.resolve('b');
    console.log(b);

    const c = await Promise.resolve('c');
    console.log(c);
}

test();
console.log('d');

// ******** 각자 따로 실행할것 ***********

new Promise(resolve => {
    console.log('a');
    resolve('b');
}).then(b => {
    console.log(b);
    return Promise.resolve('c');
}).then(c => {
    console.log(c);
});
console.log('d');

나는 여태 async 함수가 키워드 이름대로 100% 비동기일줄만 알았다. 하지만 그건 아니었고, 동기 반 비동기 반으로 진행되는 녀석이었다. 이러한 양상은 Promise의 executer 와 나머지 Promise 체인의 핸들러와 완벽히 일치한다.

첫번째 await 를 만나기 전의 웟부분이 Promise의 executor 와 같은 역할을 한다. 따라서 await 를 만나기 전에는 동기적으로 코드가 진행된다. 그러나 그 후에는 async 라는 키워드에 걸맞게 비동기적으로 진행된다. await 이후의 소스들은 Promise와 마찬가지로 Micro Task Queue 에 적재되는 것일까? 딱히 막 묶여 있지도 않는것 같은데, 고작 await 이후일 뿐인데, 적재된다면 어떤 형태로 적재될까? 매우 궁금하다.

아무튼 확실히 async-await 가 동기적으로 코드를 작성하는 깔끔한 느낌이 난다. 더욱 async-await 를 써야하는 이유를 깨닫게 되었다.

정리

Promise, async-await 에 관한 모든 궁금증이 해결되었고, 오히려 정리하다 보니 해결되는 문제도 있어서 뜻깊은 시간이었다. Promise 는 모두 정리되었는데 async-await 가 여전히 의문이긴 하다.

async 함수는 누가봐도 함수 구현체의 형태이다. 저 함수 구현체의 몸통이 어떻게 분할이 되어 Promise 같이 움직일 수 있단 말인가? Promise 는 하나의 식으로써 각각 분리가 되어있다는게 눈에 보인다. 하지만 async 함수 구현체는 일반 함수와 마찬가지로 여러개의 statement의 집할체일 뿐이다.

await 라는 키워드가 분리해주는 역할을 한 것 같은데, 내부적으로 어떤 처리를 했을까? 뭔가 코루틴의 느낌이 나기도 하기도 하지만.. 아직은 잘 모르겠다. async 와 맞물린 이 await 가 Javascript 코어에서 어떤 기술적인 처리를 하는지 아는것이 내 숙제이다.

profile
지식을 주도하는 법을 터득하는중..

0개의 댓글