이벤트 루프 다시 보기 — 태스크와 마이크로태스크
setTimeout과 Promise의 실행 순서는 왜 다를까. 마이크로태스크 큐의 동작을 정확히 이해하면 비동기 버그의 8할이 보입니다.
3분 읽기
“자바스크립트는 싱글 스레드”라는 말은 6년차라면 수백 번 들었을 겁니다. 하지만 마이크로태스크 큐를 정확히 설명할 수 있는 사람은 의외로 적습니다. 이걸 알면 await 순서, React 상태 배칭, 이벤트 핸들러 타이밍이 전부 한 그림으로 보입니다.
두 개의 큐
이벤트 루프는 한 번의 “틱”마다 이렇게 움직입니다.
- 태스크(매크로태스크) 하나를 꺼내 실행 (콜 스택이 빌 때까지)
- 그 후 마이크로태스크 큐를 전부 비운다 (비우는 도중 새로 추가된 마이크로태스크도 포함)
- 필요하면 렌더링
- 다음 태스크로
- 태스크:
setTimeout/setInterval, I/O, 사용자 이벤트,MessageChannel - 마이크로태스크:
Promise의.then/catch/finally,await이후,queueMicrotask,MutationObserver
핵심은 **“한 태스크가 끝나면 다음 태스크로 넘어가기 전에 마이크로태스크를 몽땅 비운다”**는 것입니다.
순서를 맞혀 보기
console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');
// 출력: 1 → 4 → 3 → 2
setTimeout(…, 0)이 0ms인데도 3보다 늦는 이유 — 2는 다음 태스크로 예약되고, 3은 현재 태스크가 끝난 직후 마이크로태스크로 먼저 처리되기 때문입니다.
async/await는 마이크로태스크다
await는 문법 설탕일 뿐, 뒤따르는 코드는 .then 콜백과 동일하게 마이크로태스크로 스케줄됩니다.
async function run() {
console.log('a');
await null; // 여기서 함수가 '일시정지' → 나머지는 마이크로태스크로
console.log('b');
}
run();
console.log('c');
// 출력: a → c → b
실무에서 물리는 지점
- 마이크로태스크 기아(starvation):
.then안에서 계속 새 Promise를 만들며 재귀하면 마이크로태스크 큐가 안 비워져 렌더링·이벤트가 멈춥니다. 무한 루프인데 브라우저가 “먹통”이 되는 전형적 패턴. - 렌더링 타이밍: DOM을 바꾼 직후 크기를 읽어야 한다면 마이크로태스크(
await)로는 부족할 수 있습니다. 다음 프레임이 필요하면requestAnimationFrame. - Node.js 차이: Node엔
process.nextTick이 있고, 이건 Promise 마이크로태스크보다도 먼저 처리됩니다. 브라우저엔 없는 개념이라 크로스 런타임 코드에서 주의.
규칙 하나만 기억하세요. “동기 코드 전부 → 마이크로태스크 전부 → 태스크 하나 → 다시 마이크로태스크 전부…” 이 리듬이 비동기 순서의 전부입니다.