Node.js에서 아주 짧은 비동기 후처리를 넣고 싶을 때 queueMicrotask()process.nextTick() 사이에서 잠깐 멈추는 경우가 많습니다. 둘 다 “곧 실행된다”는 느낌은 비슷하지만, 실제로는 이벤트 루프에서 끼어드는 위치와 시스템에 주는 영향이 다릅니다.

결론부터 말하면 대부분의 일반적인 후처리에는 queueMicrotask()를 우선 고려하고, Node.js 전용 API 호환이나 아주 의도적인 순서 제어가 필요할 때만 process.nextTick()을 쓰는 편이 안전합니다. 특히 process.nextTick()을 반복적으로 쌓으면 I/O보다 먼저 계속 실행돼 starvation을 만들 수 있어서, “지금 당장 빨리”라는 이유만으로 선택하면 운영 중에 발목을 잡기 쉽습니다.

queueMicrotask와 process.nextTick는 무엇이 다른가

H3. 둘 다 즉시 실행이 아니라 현재 실행이 끝난 뒤 실행된다

두 API 모두 현재 콜스택이 끝난 뒤 실행할 작업을 예약합니다. 그래서 동기 코드 중간에 바로 끼어들지는 않지만, setTimeout(fn, 0)보다 더 이른 시점에 실행되는 경우가 많습니다.

다만 중요한 차이는 어느 대기열에 들어가느냐입니다.

  • process.nextTick()은 Node.js의 next tick queue에 들어감
  • queueMicrotask()는 표준 microtask queue에 들어감
  • 둘 다 타이머보다 빠를 수 있지만, 우선순위와 공정성이 같지는 않음

즉 겉보기 용도는 비슷해도, 시스템 전체 관점에서는 성격이 꽤 다릅니다.

H3. Node.js에서는 process.nextTick가 microtask보다 더 공격적으로 먼저 돈다

Node.js에서는 process.nextTick() 큐가 매우 높은 우선순위로 처리됩니다. 이 때문에 다음과 같은 코드에서는 실행 순서가 예상과 다를 수 있습니다.

console.log('start');

queueMicrotask(() => {
  console.log('microtask');
});

process.nextTick(() => {
  console.log('nextTick');
});

Promise.resolve().then(() => {
  console.log('promise then');
});

setTimeout(() => {
  console.log('timeout');
}, 0);

console.log('end');

대체로 출력 순서는 아래처럼 나옵니다.

start
end
nextTick
microtask
promise then
timeout

세부 상황에 따라 관찰 포인트는 달라질 수 있지만, 핵심은 process.nextTick()이 너무 앞에서 돌기 때문에 다른 작업을 밀어낼 수 있다는 점입니다. 이 차이는 Node.js Event Loop Lag 모니터링 가이드를 볼 때도 중요합니다. 지연 원인을 찾다 보면 CPU 작업만 아니라 이런 “너무 촘촘한 예약”도 문제로 드러나기 때문입니다.

실무에서 queueMicrotask를 먼저 보는 이유

H3. 표준 API라 코드 의도가 더 보편적으로 읽힌다

queueMicrotask()는 브라우저와 Node.js 모두에서 통하는 표준 API입니다. 그래서 범용 JavaScript 문맥에서 읽을 때도 의도가 비교적 자연스럽습니다.

function emitAfterStateUpdate(listener, state) {
  state.ready = true;

  queueMicrotask(() => {
    listener(state);
  });
}

이 코드는 “현재 상태 변경은 끝내고, 아주 짧은 후처리를 microtask로 넘긴다”는 의미가 비교적 선명합니다. Node.js 전용 의미를 강하게 끌어오지 않아도 되기 때문에, 라이브러리성 코드나 공유 유틸에서는 특히 무난합니다.

H3. 과도한 우선순위 개입을 줄여 I/O starvation 위험이 낮다

process.nextTick()은 편리하지만 너무 쉽게 남용됩니다. 특히 재귀적으로 다시 process.nextTick()을 등록하는 코드가 있으면 I/O 단계가 충분히 돌기 전에 next tick 작업이 계속 앞줄에 서게 됩니다.

let count = 0;

function spin() {
  count += 1;

  if (count < 100000) {
    process.nextTick(spin);
  }
}

spin();

이런 패턴은 “언젠가 끝나니까 괜찮다”가 아니라, 그동안 다른 일들이 굶을 수 있다는 게 문제입니다. 운영 환경에서는 응답 지연이나 이벤트 처리 지연으로 이어질 수 있습니다. 이와 비슷한 관점은 Node.js Load Shedding 가이드처럼 시스템 공정성을 다루는 글과도 연결됩니다.

process.nextTick를 써도 되는 경우는 언제인가

H3. Node.js 콜백 스타일과의 하위 호환성이 중요할 때

오래된 Node.js API나 커스텀 라이브러리에서는 “에러든 성공이든 항상 비동기적으로 콜백을 부른다”는 계약을 맞추기 위해 process.nextTick()을 쓰는 경우가 있었습니다.

function validate(input, callback) {
  if (!input) {
    process.nextTick(() => {
      callback(new Error('input is required'));
    });
    return;
  }

  callback(null, input);
}

이 코드는 호출자가 동기 예외처럼 느끼지 않도록 실행 타이밍을 한 번 미루는 용도로 볼 수 있습니다. 다만 새 코드라면 Promise 기반 인터페이스로 옮기거나, 후처리 수준이면 queueMicrotask()로도 충분한지 먼저 보는 편이 낫습니다.

H3. 정말로 microtask보다 더 앞선 순서 보장이 필요할 때

아주 드물지만, 특정 상태 정리를 다른 Promise 콜백보다 먼저 끝내야 하는 경우가 있습니다. 그럴 때는 process.nextTick()이 의도적으로 맞을 수 있습니다.

하지만 이런 코드는 팀 안에서 오해를 부르기 쉽습니다. 그래서 저는 아래 둘 중 하나가 아니라면 보통 반대합니다.

  • Node.js 내부 동작과 맞물린 순서 제어가 명확히 필요함
  • 기존 계약을 깨지 않기 위해 제한적으로 유지해야 함

그 외라면 nextTick은 “빨라 보이는 해법”이지 “좋은 기본값”은 아닙니다.

헷갈리기 쉬운 선택 기준

H3. Promise.then과 queueMicrotask는 같은 microtask 계열로 봐도 된다

Promise.resolve().then(fn)도 사실상 microtask 기반 후처리입니다. 그래서 단순 예약만 필요하다면 queueMicrotask(fn)가 더 직접적인 표현일 수 있습니다.

queueMicrotask(() => flushBufferedLogs());

이 코드는 “Promise를 만들려는 것”이 아니라 “microtask 하나를 예약하려는 것”임을 바로 보여 줍니다. 불필요한 Promise 체인을 만들지 않아도 돼 의도 전달이 깔끔합니다.

비동기 흐름 자체를 설계하는 문제라면 Node.js Promise.withResolvers 가이드도 함께 읽어볼 만합니다. 하나는 예약 순서 문제이고, 다른 하나는 완료 시점 제어 문제라서 실무에서 같이 등장하는 경우가 많습니다.

H3. setImmediate나 setTimeout(0)와는 목적이 다르다

setImmediate()setTimeout(fn, 0)는 이벤트 루프의 다음 페이즈나 타이머 단계로 넘기는 쪽에 가깝습니다. 반면 queueMicrotask()process.nextTick()은 그보다 더 빠른 후처리입니다.

그래서 아래처럼 구분하면 실수를 줄이기 좋습니다.

  • 아주 짧은 후처리, 상태 정리, 콜백 일관성 유지 → queueMicrotask() 우선 검토
  • Node.js 특화된 강한 순서 제어 → 제한적으로 process.nextTick() 검토
  • I/O에 숨 돌릴 틈을 주고 다음 턴으로 넘기기 → setImmediate() 또는 타이머 계열 검토

즉 “더 빠르다”보다 “어느 계층의 예약인가”로 구분하는 편이 정확합니다.

안전하게 쓰기 위한 실무 체크리스트

H3. nextTick를 반복 등록하는 코드부터 의심하자

코드 리뷰에서 아래 패턴이 보이면 한 번 더 확인하는 편이 좋습니다.

  • 루프 안에서 process.nextTick()을 반복 등록함
  • 재귀 함수가 매번 process.nextTick()으로 자신을 다시 부름
  • 에러 처리, 재시도, 큐 flush가 모두 nextTick에 걸려 있음
  • 이유 설명 없이 “비동기로 바꾸려고” 그냥 nextTick을 사용함

이런 경우는 대부분 더 좋은 기본값이 있습니다. 필요하다면 concurrency 제한, 큐, 배치 처리로 다시 모델링하는 편이 낫습니다.

H3. 대부분의 애플리케이션 코드는 queueMicrotask면 충분하다

실무 기준으로 정리하면 저는 아래처럼 권합니다.

  1. 브라우저/Node.js 모두 고려하거나 일반 후처리라면 queueMicrotask()부터 본다.
  2. Promise 체인을 만들 필요가 없다면 Promise.resolve().then()보다 queueMicrotask()가 더 직접적이다.
  3. process.nextTick()은 Node.js 전용 계약이나 강한 순서 보장이 필요할 때만 쓴다.
  4. 반복적 예약이 보이면 event loop starvation 가능성을 먼저 점검한다.

이 기준만 지켜도 “왜 이 코드가 I/O를 이상하게 막지?” 같은 문제를 꽤 줄일 수 있습니다.

마무리

queueMicrotask()process.nextTick()은 둘 다 짧은 후처리를 예약하는 도구지만, Node.js에서의 무게감은 전혀 같지 않습니다. process.nextTick()은 강력한 대신 너무 앞에서 실행돼 시스템 공정성을 해치기 쉽고, queueMicrotask()는 보통 더 예측 가능하고 표준적인 선택지입니다.

그래서 새 코드에서는 queueMicrotask()를 기본값으로 두고, 정말 필요한 이유가 있을 때만 process.nextTick()으로 내려가는 접근이 실무적으로 안전합니다. 속도보다 중요한 건, 이벤트 루프 전체를 망치지 않는 순서 제어입니다.

관련 글: