Quick Reference
console.log("A");
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
queueMicrotask(() => console.log("microtask"));
setImmediate(() => console.log("immediate"));
console.log("B");항상 보장되는 부분은 현재 동기 코드 A, B가 먼저 끝나고, 그 뒤 Promise와 queueMicrotask가 처리된다는 점입니다. setTimeout(0)과 setImmediate() 중 무엇이 먼저인지는 호출 위치와 I/O 상태에 따라 달라지므로 순서 계약으로 쓰지 않습니다. 오래 걸리는 CPU 작업은 어느 큐를 쓰든 이벤트 루프를 막습니다.
- 현재 동기 코드가 먼저 끝난다
- 그 직후 microtask를 비운다
- timers와 I/O 계열의 상대 순서는 호출 경계에 따라 달라진다
실행 순서
이벤트 루프를 다시 읽을 때 먼저 붙잡으면 좋은 기본 축은 아래입니다.
- 현재 동기 코드
process.nextTick- Promise /
queueMicrotask - timer (
setTimeout) - I/O 이후
setImmediate
Promise / queueMicrotask / nextTick
이 셋은 "지금 콜백이 끝난 직후"와 관련 있습니다. Promise와 queueMicrotask는 현재 JavaScript 작업이 끝난 뒤 처리되며 timers보다 먼저 보입니다. process.nextTick은 Node가 다음 이벤트 루프 단계로 넘어가기 전에 별도로 비우므로 더 앞섭니다.
핵심은 "이벤트 루프 다음 바퀴"보다 "지금 바퀴가 끝나기 전 정리 작업"에 가깝다는 점입니다.
setTimeout(() => console.log("timeout"), 0);
Promise.resolve().then(() => console.log("promise"));
process.nextTick(() => console.log("nextTick"));이 예제처럼 같은 최상위 경계에서 만든 작업은 nextTick -> Promise/queueMicrotask -> timer 순서 감각으로 읽을 수 있습니다. 다만 I/O 콜백 안에서 만든 timer와 immediate의 상대 순서까지 이 규칙으로 일반화하면 안 됩니다.
즉 nextTick은 "조금 뒤"라기보다 "이벤트 루프가 다른 일로 넘어가기 전에 먼저 비워지는 큐"로 보는 편이 맞습니다.
setTimeout(0) vs setImmediate
둘은 둘 다 "나중"이지만 질문이 다릅니다.
setTimeout(0): timers 단계setImmediate: check 단계
특히 I/O 콜백 안에서는 setImmediate가 먼저 보입니다.
import { readFile } from "node:fs";
readFile("./data.txt", () => {
setTimeout(() => console.log("timeout"), 0);
setImmediate(() => console.log("immediate"));
});이 경우에는 setImmediate가 먼저 처리됩니다. 그래서 "무조건 timeout이 먼저"로 외우기보다 "지금 I/O 콜백 안인가"를 같이 봐야 합니다.
CPU 작업
이벤트 루프를 막는 것은 결국 동기 CPU 작업
function dangerousSum(n) {
let total = 0;
for (let i = 0; i < n; i++) total += i;
return total;
}이런 코드는 await가 없어도 "한 스레드 전체"를 오래 붙잡습니다. 그래서 Node에서 이벤트 루프 카드는 곧 CPU 바운드 작업 경계 카드이기도 합니다.
대응은 보통 이 셋 중 하나입니다.
- 작업을 청크로 나누기
setImmediate같은 지점으로 제어권 양보- 진짜 CPU 바운드면
worker_threads
순서를 외우기보다 "지금 어느 경계 안인가"를 본다
이벤트 루프 카드는 전체 phase 이름을 외우는 카드보다, 현재 동기 실행 안인지, I/O 콜백 안인지, microtask를 계속 쌓고 있는지 판단하는 카드에 가깝습니다.
무엇이 먼저 도나
체크포인트
- 동기 코드가 먼저 끝난다
- microtask가 timers보다 앞선다
- I/O 콜백 안에서는
setImmediate가 timer보다 먼저 처리된다 - CPU 바운드 작업은 이벤트 루프 전체를 막는다
nextTick은 너무 많이 쌓으면 다른 작업을 굶길 수 있다
주의할 점
process.nextTick이나 Promise 체인을 재귀적으로 계속 이어 붙이면 "비동기"처럼 보여도 실제로는 이벤트 루프가 다른 작업으로 넘어갈 틈을 잃습니다. 순서 제어용으로 조금 쓰는 것과, 루프를 굶기는 것은 완전히 다른 문제입니다.
참고 링크
2 sources