microtask queue는 현재 실행 중인 동기 코드가 끝난 직후, 다음 task로 넘어가기 전에 먼저 처리되는 비동기 콜백 큐입니다. 대표적으로 Promise.then, queueMicrotask, MutationObserver 콜백이 여기에 들어갑니다.
그래서 같은 코드 흐름에서는 setTimeout(..., 0)으로 등록한 timer보다 Promise 콜백이 먼저 실행됩니다. 다만 microtask queue는 빌 때까지 계속 처리되기 때문에, microtask를 과도하게 이어 붙이면 timer나 rendering이 밀릴 수 있습니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
Promise.then 안에서 setTimeout을 만들고, setTimeout 안에서 Promise.then을 만들면 순서를 어떻게 설명할 건가요?
핵심 규칙은 “지금 실행 중인 task가 끝날 때마다 microtask queue를 비운다”는 점입니다.
처음 script task가 끝나면 먼저 promise 1이 실행됩니다. 이때 새 timer가 등록됩니다. 이후 timer task로 넘어가 timeout 1이 실행되고, 그 task가 끝나기 전에 생긴 promise in timeout은 다음 timer로 넘어가기 전에 먼저 실행됩니다. Promise가 항상 setTimeout보다 먼저인 것이 아니라, 각 task 경계마다 microtask checkpoint가 있습니다.
microtask 안에서 계속 microtask를 추가하면 브라우저는 렌더링을 할 수 있나요?
어려워집니다. microtask queue는 비워질 때까지 계속 처리되기 때문에, microtask가 다시 microtask를 계속 추가하면 렌더링이나 다음 task가 밀릴 수 있습니다. 그래서 queueMicrotask나 긴 Promise chain을 무한히 이어붙이면 화면이 멈춘 것처럼 보일 수 있습니다.
이 질문에서는 “microtask가 빠르다”에서 끝내지 말고, 너무 많이 쓰면 렌더링 기회를 막을 수 있다는 점까지 말하는 게 좋습니다.
async/await에서 await 다음 코드는 microtask와 어떤 관계가 있나요?
await 뒤의 이어지는 코드는 Promise reaction과 비슷하게 나중에 이어집니다. 그래서 동기 코드가 모두 끝난 뒤 이어지는 흐름으로 보면 됩니다.
async function run() { console.log("A"); await null; console.log("B");}run();console.log("C");
출력은 A → C → B입니다. await가 thread 전체를 막는 것이 아니라, async 함수의 나머지 부분을 나중에 이어서 실행하게 만든다고 설명하면 됩니다.
microtask와 macrotask라는 말은 표준 용어 그대로인가요?
macrotask는 자료에서 많이 쓰이지만, HTML spec 쪽에서는 보통 task와 microtask처럼 설명합니다. macrotask는 관용적으로 쓰는 표현에 가깝고, timer나 event callback이 들어가는 task를 가리킬 때 자주 쓰입니다.
MutationObserver가 microtask라면, DOM 변경 직후 바로 화면이 바뀐 뒤에 호출되나요?
보통은 렌더링 전에 microtask checkpoint에서 처리된다고 이해하면 됩니다. DOM을 바꿨다고 해서 즉시 paint가 끝나는 것이 아니고, JS task가 끝나고 microtask를 처리한 뒤 브라우저가 렌더링 기회를 갖습니다. 그래서 MutationObserver callback 안에서 다시 DOM을 만지면 실제 paint 전에 변경이 이어질 수 있습니다.
Node.js에서도 브라우저와 완전히 같은 순서라고 말해도 되나요?
브라우저와 Node.js의 큰 흐름은 비슷하지만 완전히 같지는 않습니다. Node.js에는 process.nextTick 같은 별도 우선순위와 event loop phase가 있어 런타임마다 세부 순서가 달라질 수 있습니다.
부가 설명
처음 이 순서를 보면 setTimeout(..., 0)이 제일 먼저 실행될 것처럼 느껴질 수 있습니다. 시간이 0ms니까요. 그런데 0ms는 “즉시 실행”이 아니라, timer task를 다음 macrotask queue에 넣겠다는 뜻에 가깝습니다.
반대로 Promise 콜백은 microtask queue에 들어갑니다. 현재 실행 중인 스크립트가 끝나고 call stack이 비면, event loop는 렌더링이나 다음 timer callback으로 넘어가기 전에 microtask를 먼저 확인합니다. 그리고 microtask queue가 비워질 때까지 계속 실행합니다.