🔄

이벤트 루프 — setTimeout(fn, 0)이 즉시 실행되지 않는 이유

Call Stack, Task Queue, Microtask Queue의 실행 순서

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 출력: 1, 4, 3, 2

0ms 타임아웃인데 왜 Promise보다 뒤에 나오는가? 이벤트 루프의 실행 순서 때문이다.

3가지 큐

Call Stack — 현재 실행 중인 함수. 동기 코드는 여기서 순차 실행된다.

Microtask Queue — Promise.then, MutationObserver, queueMicrotask. Call Stack이 비면 모든 Microtask를 다 비울 때까지 실행. Task Queue보다 우선.

Task Queue (Macro Task) — setTimeout, setInterval, I/O 콜백, requestAnimationFrame. Microtask가 전부 비워진 후에 하나씩 실행.

실행 순서

  1. Call Stack의 동기 코드를 전부 실행 → 1, 4 출력
  2. Call Stack이 비었다 → Microtask Queue 확인 → Promise.then 실행 → 3 출력
  3. Microtask Queue도 비었다 → Task Queue 확인 → setTimeout 콜백 실행 → 2 출력

V8에서의 구현

V8 자체는 이벤트 루프를 구현하지 않는다. 브라우저에서는 libevent/OS 이벤트 루프가, Node.js에서는 libuv가 담당한다. V8은 Call Stack과 Microtask Queue만 관리.

setTimeout(fn, 0)의 실제 최소 지연은 브라우저에서 4ms다 (HTML spec). 0ms를 지정해도 4ms 이상 걸린다.

무한 Microtask

function loop() { Promise.resolve().then(loop); }
loop(); // 브라우저 멈춤!

Microtask Queue가 비워지기 전에 새 Microtask가 계속 추가되면 Task Queue에 영원히 차례가 안 온다. UI 업데이트도 안 된다 — 렌더링도 Task이기 때문.

핵심 포인트

1

Call Stack의 동기 코드가 전부 끝나야 비동기 콜백이 실행된다

2

Microtask(Promise.then)가 Task(setTimeout)보다 항상 먼저 실행

3

Microtask Queue는 전부 비워질 때까지 실행 — 중간에 Task 안 끼어듦

4

V8은 Call Stack + Microtask만 관리, 이벤트 루프는 브라우저/libuv가 담당

사용 사례

비동기 실행 순서 디버깅 — 왜 이 콜백이 저것보다 먼저 실행되는가 UI 응답성 — 무거운 작업을 setTimeout으로 쪼개서 렌더링 기회 확보