← 글 목록

notes

Event Loop 심화 — microtask vs macrotask

콜 스택·마이크로태스크·매크로태스크를 Flux 일기 말투로 정리. then이 setTimeout(0)보다 먼저인 이유.

참고: WHATWG HTML — Event loops, MDN — Using microtasks, Jake Archibald “In The Loop”

Deep Dive 45·46장(프로미스·async) 옆에 붙이는 런타임 스케줄 메모.

“비동기면 백그라운드 스레드가 도는 거지?”

예전에 그렇게 대충 넘겼다. 콘솔에 setTimeout이랑 Promise.then을 같이 찍는 순간 순서가 뒤집혀서 Ment붕.

찾아보니 이벤트 루프는 멀티스레드가 아니라, 한 손(콜 스택)으로 할 일 메모를 갈아 끼우는 스케줄러에 가깝다.

async/await는 쓰는데, “왜 then이 setTimeout(0)보다 먼저냐”를 말로 설명 못 했다.

MDN·스펙을 보니 큐가 하나가 아니었다.

태스크(매크로) 랑 마이크로태스크 가 갈린다.

이게 무슨 말이냐.

콜 스택이 비는 순간, 엔진은 먼저 마이크로태스크 큐를 바닥날 때까지 비운다.

그다음에야 매크로태스크(예: setTimeout 콜백)를 하나 가져와 스택에 올린다.

그래서 같은 “0ms 뒤”라도 then 쪽이 먼저인 경우가 많다.

flowchart LR
  S[Call Stack] -->|비면| M[Microtask Queue<br/>Promise.then / queueMicrotask]
  M -->|모두 비움| T[Task Queue<br/>setTimeout / I/O]
  T -->|하나 실행| S

손으로 확인한 순서

console.log('1 sync');

setTimeout(() => console.log('2 timeout'), 0);

Promise.resolve().then(() => console.log('3 then'));

queueMicrotask(() => console.log('4 micro'));

console.log('5 sync end');
// 1 → 5 → 3 → 4 → 2

이게 무슨 말이냐.

동기 코드가 스택을 점유하는 동안 타이머·then은 큐에만 쌓인다.

스택이 비면 마이크로(3,4)를 다 처리하고, 그제야 타이머(2).

렌더와의 관계 (브라우저)

브라우저 입장에선 마이크로태스크가 너무 길게 이어지면 렌더 기회가 밀릴 수 있다.

“then 안에서 then을 끝없이 스케줄”하면 UI가 버벅일 수 있다 — 이론만이 아니라 실무에서 로그 폭주 버그로 만났다.

await는 뭐냐

await는 그 지점 뒤 코드를 마이크로태스크처럼 이어 가게 만드는 설탕에 가깝다.

그래서 await 다음 줄이 setTimeout(0)보다 먼저 도는 장면을 또 본다.

async function f() {
  console.log('A');
  await null;
  console.log('B'); // 마이크로 쪽
}
f();
setTimeout(() => console.log('C'), 0);
console.log('D');
// A D B C

헷갈리던 질문 → 자답

Q. 워커 스레드는?

A. 진짜 다른 스레드. 이벤트 루프 이야기와 층을 섞으면 안 된다. postMessage로 만난다.

Q. Node도 같냐?

A. 큰 그림은 비슷하지만 페이즈(timers, poll, check…)가 더 있다. “브라우저 그림”을 그대로 Node에 붙이면 어긋날 수 있다.

Q. 마이크로를 하나로 합치면?

A. queueMicrotask / Promise.then이 같은 급. setImmediate(Node)나 setTimeout은 태스크 급.

여기서 잠깐 💁‍♂️

비동기 = 빠른 마법이 아니다.

언제 큐에 넣고, 어느 큐에 넣느냐의 문제다.

디버깅할 때 1 sync처럼 번호를 찍어 순서를 고정하면, “느낌”이 “측정”으로 바뀐다.