본문 바로가기
C.W.K.
Stream
Lesson 04 of 05 · published

마이크로태스크와 매크로태스크 — 두 실행 큐

~14 min · async, microtasks, scheduling, event-loop

Level 0노드 입문자
0 XP0/40 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Node의 비동기 작업은 서로 다른 큐에서 차례를 기다려. 어느 큐에 들어가는지 알면 실행 순서를 맞힐 수 있고, 한 큐가 I/O를 굶기는 버그도 찾을 수 있어."

두 종류의 큐

이벤트 루프가 처리하는 일은 크게 매크로태스크와 마이크로태스크로 나눌 수 있어:

  • 매크로태스크 — 이벤트 루프의 각 단계가 맡은 일이야. Timers 단계는 setTimeoutsetInterval 콜백을, Poll 단계는 I/O 콜백을, Check 단계는 setImmediate 콜백을 처리해.
  • 마이크로태스크 — 현재 JavaScript 작업이 끝난 직후 처리할 일을 모아 둔 별도 큐야. queueMicrotask(fn), 프로미스의 .then, await 뒤에 이어지는 코드가 여기에 들어가. Node에는 이보다 먼저 비우는 process.nextTick 전용 큐도 있어.

큐를 비우는 순서

현재 콜백이 끝나면 Node는 process.nextTick 큐를 먼저 비우고, 이어서 프로미스 마이크로태스크 큐를 비운 뒤 이벤트 루프의 다음 일로 넘어가. 그래서 같은 동기 코드에서 예약한 Promise.resolve().then(fn)은 보통 타이머나 I/O 콜백보다 먼저 실행돼.

마이크로태스크가 실행될 때마다 또 다른 마이크로태스크를 끝없이 추가하면 문제가 생겨. 큐가 비지 않으니 이벤트 루프가 다음 단계로 넘어가지 못하고 서버가 I/O를 처리할 기회도 잃어. 이 현상을 마이크로태스크 기아라고 해.

한 번에 따라가 보기

동기 코드 한 덩어리가 끝난 뒤의 순서를 단순화하면 이래:

  1. 현재 동기 코드를 끝까지 실행해.
  2. process.nextTick 콜백을 등록된 순서대로 처리해.
  3. queueMicrotask, 프로미스의 .then, await 뒤 코드 같은 마이크로태스크를 처리해.
  4. 이벤트 루프의 다음 단계에서 타이머나 I/O, setImmediate 같은 일을 처리해.
  5. 그 콜백이 끝나면 다시 2번과 3번의 큐를 비워.

핵심은 마이크로태스크를 이벤트 루프 한 바퀴가 끝날 때 한 번만 처리하는 게 아니라는 점이야. 각 JavaScript 콜백이 끝날 때마다 작은 우선 큐를 먼저 비운다고 생각하면 실행 순서를 훨씬 정확히 그릴 수 있어.

await 뒤가 곧바로 이어지는 듯한 이유

await 뒤의 코드는 프로미스가 끝났을 때 마이크로태스크로 예약돼. 현재 실행 중인 코드와 앞서 등록된 우선 작업이 끝나면 곧바로 함수가 다시 이어지니, 다음 이벤트 루프까지 오래 기다리는 것처럼 느껴지지 않는 거야.

그렇다고 await가 CPU 계산을 잘게 나눠 주지는 않아. await 뒤에서 오래 도는 동기 계산을 시작하면 그 마이크로태스크가 끝날 때까지 I/O는 여전히 기다려야 해.

setImmediatequeueMicrotask 고르기

I/O가 먼저 실행될 기회를 주고 싶다면 setImmediate(fn)을 써. 콜백이 Check 단계로 넘어가므로 그 전에 Poll 단계의 I/O를 처리할 수 있어. 반대로 현재 동기 코드가 끝나자마자 다른 I/O보다 먼저 이어서 실행해야 한다면 queueMicrotask(fn)이 맞아. 우선순위는 프로미스의 .then과 같아.

많은 항목을 덩어리로 나눠 처리하는 루프라면 덩어리 사이에 setImmediate를 넣어 봐. 이벤트 루프가 다른 연결과 I/O도 돌볼 틈을 얻어.

Pippa의 고백

오랫동안 나는 비동기 코드라면 이벤트 루프가 알아서 공평하게 나눠 줄 거라고 생각했어. 아빠가 보여 준 서버 추적 기록에는 작은 CPU 작업을 하는 .then() 50개가 한꺼번에 이어져 있었지. 각 작업은 짧았지만 모두 마이크로태스크 큐에서 이어져 80ms 동안 I/O가 멈췄어. 중간에 setImmediate(...)를 한 번 넣자 다른 작업도 숨을 돌릴 수 있었어. 이제 긴 프로미스 연쇄나 빡빡한 for await 루프를 보면 먼저 물어. “중간에 I/O가 실행될 틈을 줬나?”

Code

Scheduler 여섯, 한 tick — 검증된 순서·javascript
// Predict the order. Then run it and check yourself.

console.log('1: sync start');

setTimeout(() => console.log('A: setTimeout 0'), 0);
setImmediate(() => console.log('B: setImmediate'));

process.nextTick(() => console.log('C: nextTick'));
queueMicrotask(() => console.log('D: queueMicrotask'));
Promise.resolve().then(() => console.log('E: promise.then'));

async function quick() {
  await Promise.resolve();
  console.log('F: after await');
}
quick();

console.log('2: sync end');

// Expected output (Node 26):
// 1: sync start
// 2: sync end
// C: nextTick
// D: queueMicrotask
// E: promise.then
// F: after await
// B: setImmediate    (or A first if timer is 'expired enough')
// A: setTimeout 0
긴 async 루프 안에서 I/O 한테 yield·javascript
// Microtask starvation in the wild
// This LOOKS innocent but blocks the event loop.
async function processAll(items) {
  for (const item of items) {
    await processOne(item);  // each iteration runs in the microqueue
  }
}

// 10000 items, each fast — but I/O is starved for the whole duration.
// Fix: yield to the loop periodically
async function processAllNice(items) {
  for (let i = 0; i < items.length; i++) {
    await processOne(items[i]);
    if (i % 100 === 0) {
      // Macrotask break — let I/O happen
      await new Promise(r => setImmediate(r));
    }
  }
}

External links

Exercise

작은 HTTP 서버에 / 경로를 만들고, 응답 전에 동기 for 루프로 Math.random()을 천만 번 호출해. curl 두 개로 동시에 요청하면 둘 다 기다리게 돼. 이제 루프를 10만 번 단위로 나누고 덩어리 사이마다 await new Promise(resolve => setImmediate(resolve))를 넣어. 다시 요청하면 두 작업이 번갈아 진행되는 걸 볼 수 있어.
Hint
처음 버전은 CPU 작업 하나가 이벤트 루프를 붙잡아서 뒤의 요청을 모두 세워. 나눈 버전은 덩어리 사이에 I/O를 처리할 틈을 줘. 전체 처리량은 양보 비용만큼 조금 줄 수 있지만, 동시에 들어온 요청의 대기 시간은 크게 줄어.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.