이벤트 루프 — 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가 전부 비워진 후에 하나씩 실행.
실행 순서
- Call Stack의 동기 코드를 전부 실행 →
1,4출력 - Call Stack이 비었다 → Microtask Queue 확인 → Promise.then 실행 →
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이기 때문.
핵심 포인트
Call Stack의 동기 코드가 전부 끝나야 비동기 콜백이 실행된다
Microtask(Promise.then)가 Task(setTimeout)보다 항상 먼저 실행
Microtask Queue는 전부 비워질 때까지 실행 — 중간에 Task 안 끼어듦
V8은 Call Stack + Microtask만 관리, 이벤트 루프는 브라우저/libuv가 담당