Node.js · 심화
스트림·운영·프로파일링
이벤트 루프 단계와 블로킹 찾기
timers/poll/check 단계, setImmediate 와 setTimeout 순서, process.nextTick 과 마이크로태스크, 이벤트 루프 지연 측정(perf_hooks monitorEventLoopDelay)
개발자KR · 원고 갱신
이 장에서 배우는 것
Node.js는 단일 스레드(single thread) 구조를 채택하여 적은 메모리로 수많은 동시 연결을 처리한다. 앞 장에서 다룬 바이트 배열과 문자열 인코딩 변환, HTTP 요청 수신, 파일 시스템 접근 등의 작업은 모두 이 단일 스레드 위에서 조율된다. 메인 스레드가 멈추지 않고 비동기(asynchronous) I/O 작업을 운영체제에 위임할 수 있는 이유는 백그라운드에서 실행 시점을 관리하는 이벤트 루프(event loop)가 존재하기 때문이다.
하지만 개발자가 작성한 자바스크립트 코드가 단일 스레드에서 오랜 시간 연산을 수행하면 이벤트 루프는 정지한다. 운영체제가 비동기 작업의 완료를 알려도, 메인 스레드가 이전 작업을 끝내지 못하면 콜백 함수는 실행 대기열에 쌓이기만 한다. 이러한 현상을 블로킹(blocking)이라고 부르며, 고성능 서버를 설계할 때 가장 경계해야 할 문제다.
서버 운영 중 발생하는 간헐적인 지연이나 원인을 알 수 없는 타임아웃은 대부분 잘못 배치된 동기 코드와 이벤트 루프의 병목에서 기인한다. 이 장에서는 엔진 내부 깊숙한 곳에서 작업이 어떤 순서로 큐에 담기고 실행되는지 파악하고, 블로킹을 추적하는 방법을 학습한다.
- 이벤트 루프를 구성하는 6가지 단계와 C++ 라이브러리 libuv의 역할
- 마이크로태스크(microtask) 큐와 매크로태스크(macrotask) 큐의 우선순위 차이
- setImmediate와 setTimeout 함수의 실행 순서가 보장되는 조건
- 내장 모듈인 perf_hooks를 활용하여 런타임 지연 시간을 측정하고 관측하는 방법
문제 상황
우리가 점진적으로 구축하고 있는 로그 수집 서버는 평소 초당 수천 건의 작은 로그 요청을 무리 없이 처리한다. 클라이언트가 HTTP POST 메서드로 전송하는 접속 로그는 라우터에서 수신되어 버퍼에 담기고, 최종적으로 JSON 문자열로 변환된다. 대부분의 로그는 크기가 수백 바이트 단위이므로 파싱과 응답에 밀리초 단위의 시간조차 걸리지 않는다.
그러나 최근 특정 외부 시스템이 한 번에 수십 메가바이트 크기의 중첩된 JSON 배열을 전송하기 시작했다. Node.js 표준 라이브러리의 JSON 객체는 문자열을 동기적으로 파싱한다. 메인 스레드가 이 거대한 문자열 구조를 분석하고 자바스크립트 객체로 변환하는 데 150밀리초가 소요된다고 가정해 보자. 이 150밀리초 동안 이벤트 루프는 파싱 연산에 갇혀 완전히 멈춰 선다.
이 짧아 보이는 멈춤은 서비스 전체에 연쇄적인 장애를 일으킨다. 이벤트 루프가 멈춘 동안 수백 명의 다른 사용자가 전송한 정상적인 로그 요청은 운영체제의 네트워크 수신 버퍼와 Node.js 내부 큐에 그대로 적체된다. 더 심각한 문제는 인프라 환경의 로드 밸런서(load balancer)가 주기적으로 서버의 정상 동작 여부를 묻는 상태 확인(health check) 요청마저 대기 상태에 빠진다는 점이다.
상태 확인 요청에 대한 응답이 설정된 임계치를 초과하여 지연되면, 로드 밸런서는 해당 서버 인스턴스가 고장 났다고 판단하여 네트워크 트래픽 할당을 중단한다. 결국 단 하나의 무거운 동기 파싱 작업 때문에 멀쩡하게 돌아가던 서버가 통째로 서비스망에서 제외되는 결과를 낳는다. 이러한 상황을 방지하려면 비동기 흐름의 기반이 되는 이벤트 루프의 실행 단계를 정확히 이해하고, 병목을 찾아내는 모니터링 체계를 갖추어야 한다.
이벤트 루프의 6가지 단계
Node.js의 이벤트 루프는 자바스크립트를 해석하는 V8 엔진이 아니라, 비동기 I/O를 지원하기 위해 작성된 C 언어 라이브러리인 libuv 내부에서 구동된다. libuv는 운영체제 커널이 제공하는 이벤트 통지 메커니즘(Linux의 epoll, macOS의 kqueue 등)을 추상화하여 단일한 비동기 인터페이스를 제공한다.
이벤트 루프는 단순히 하나의 큐를 순회하는 것이 아니라, 각기 다른 역할을 수행하는 6개의 단계(phase)를 가진다. 루프가 한 단계를 처리하고 다음 단계로 이동하는 전체 과정을 틱(tick)이라고 부른다. 각 단계는 자신만의 큐(또는 힙 메모리 구조)를 가지고 있으며, 해당 단계에 진입하면 큐에 쌓인 콜백 함수들을 모두 실행하거나 시스템 실행 한도에 도달할 때까지 작업을 처리한다.
첫 번째는 타이머(timers) 단계다. 이 단계에서는 setTimeout과 setInterval 함수를 통해 예약된 콜백들이 실행된다. 내부적으로는 최소 힙(min-heap) 자료구조를 사용하여 예약된 시간이 가장 빠른 타이머부터 검사한다. 설정한 지연 시간이 운영체제의 현재 시간 기준을 지났다면 해당 콜백을 큐에서 꺼내어 실행한다.
두 번째는 대기 콜백(pending callbacks) 단계다. 이 단계는 이전 틱에서 처리가 미뤄진 시스템 운영체제 수준의 콜백을 실행한다. 예를 들어 TCP 소켓이 연결을 시도하다가 운영체제 레벨에서 오류가 발생했을 때, 그 오류를 보고하는 콜백이 이 단계에서 호출된다. 일반적인 애플리케이션 개발자가 이 단계의 동작에 직접 관여하는 일은 드물다.
세 번째는 유휴 및 준비(idle, prepare) 단계다. 이 단계는 Node.js 런타임이 내부적인 상태 관리와 다음 단계를 준비하기 위해 사용한다. 자바스크립트 코드에서 직접 접근할 수 없으며, 이벤트 루프가 스스로 폴 단계를 준비하는 엔진 내부의 과정이다.
네 번째는 폴(poll) 단계이며 이벤트 루프에서 가장 길고 중요한 역할을 담당한다. 이 단계에서는 운영체제에 위임했던 네트워크 요청의 완료, 파일 읽기 완료 등의 새로운 I/O 이벤트를 회수하고 콜백을 실행한다. 폴 큐에 실행할 콜백이 있다면 큐가 비워질 때까지 동기적으로 모두 실행한다. 만약 큐가 비어있고 다음에 실행할 예약된 타이머나 확인 단계의 작업이 없다면, 이벤트 루프는 다음 I/O 이벤트가 발생할 때까지 이 폴 단계에서 대기(블로킹)하며 스레드를 쉬게 한다.
다섯 번째는 확인(check) 단계다. 오직 setImmediate 함수로 등록된 콜백만을 처리하기 위해 존재하는 전용 단계다. 폴 단계에서 I/O 콜백을 실행하던 중 setImmediate가 호출되면, 이벤트 루프는 폴 단계를 마치자마자 대기하지 않고 즉시 확인 단계로 이동하여 해당 콜백을 실행한다.
여섯 번째는 닫기 콜백(close callbacks) 단계다. 소켓이나 스트림의 연결이 갑자기 종료되었을 때 발생하는 'close' 이벤트 콜백들이 여기서 실행된다. 리소스를 정리하고 메모리 누수를 방지하는 마무리 작업이 주로 이루어진다.
마이크로태스크 큐와 process.nextTick
앞서 살펴본 6가지 단계는 libuv 라이브러리가 관리하는 매크로태스크 큐 영역이다. 이와 별개로 Node.js는 이벤트 루프의 공식 단계에는 속하지 않지만, 각 단계 사이사이에 개입하여 최우선으로 실행되는 마이크로태스크(microtask) 큐를 직접 관리한다.
마이크로태스크 큐는 두 가지 종류로 나뉜다. 첫 번째는 process.nextTick 함수를 통해 등록된 콜백들이 모이는 큐(nextTickQueue)다. 두 번째는 자바스크립트 기본 객체인 Promise의 then, catch, finally 핸들러가 모이는 큐(microTaskQueue)다.
이벤트 루프가 현재 실행 중인 연산을 마치거나 특정 단계(phase)에서 다음 단계로 넘어가려 할 때, Node.js 엔진은 항상 마이크로태스크 큐를 먼저 검사한다. 큐에 작업이 존재하면 이벤트 루프의 진행을 잠시 멈추고 마이크로태스크 큐에 있는 모든 콜백을 순차적으로 실행한다. 여기서 중요한 규칙은 process.nextTick 큐가 Promise 큐보다 항상 높은 우선순위를 가진다는 점이다.
마이크로태스크 큐의 실행 방식에는 독특한 특징이 있다. 큐에 들어있는 작업을 처리하는 도중에 새로운 마이크로태스크가 추가되면, 엔진은 큐가 완전히 비워질 때까지 계속해서 콜백을 실행한다. 만약 process.nextTick 내부에서 다시 process.nextTick을 재귀적으로 호출하는 코드를 작성하면, 엔진은 영원히 마이크로태스크 큐에서 빠져나오지 못한다. 결과적으로 이벤트 루프의 타이머나 폴 단계로 진입하지 못하므로, 새로운 네트워크 요청을 받거나 타이머를 실행할 수 없는 상태인 이벤트 루프 기아(starvation) 현상이 발생한다.
setImmediate와 setTimeout의 실행 순서
비동기 작업을 다음 틱으로 미룰 때 개발자들은 흔히 setImmediate 콜백과 지연 시간을 0으로 설정한 setTimeout(fn, 0)을 비교하여 사용한다. 두 함수는 비슷해 보이지만 이벤트 루프 내에서 소속된 단계가 다르기 때문에 실행 컨텍스트에 따라 호출 순서가 달라진다.
먼저 메인 모듈의 최상단(루트 스코프)에서 두 함수를 동시에 호출하는 경우를 살펴보자. 이론상 지연 시간이 0인 타이머는 즉시 실행되어야 할 것 같지만, Node.js 내부 규정상 setTimeout의 최소 지연 시간은 1밀리초로 보정된다. 메인 스크립트가 처음 실행을 마치고 이벤트 루프가 활성화될 때, 시스템의 성능이나 당시 운영체제의 부하에 따라 1밀리초가 이미 지났을 수도 있고 지나지 않았을 수도 있다.
만약 프로세스 시작 후 1밀리초가 경과했다면 이벤트 루프는 타이머 단계를 먼저 처리하여 setTimeout의 콜백을 실행하고, 이후 단계를 거쳐 확인 단계에서 setImmediate를 실행한다. 반대로 컴퓨터 성능이 매우 뛰어나 1밀리초 이내에 타이머 단계에 도달했다면, 타이머 큐가 비어있다고 판단하고 다음 단계들로 넘어간다. 그리고 확인 단계에서 setImmediate를 먼저 실행한 뒤, 다음 틱의 타이머 단계에서 setTimeout을 실행한다. 이처럼 최상단 스코프에서는 두 함수의 실행 순서가 비결정적(non-deterministic)이며 매 실행마다 결과가 달라질 수 있다.
반면 파일 읽기나 HTTP 요청 처리 같은 I/O 콜백 내부에서 두 함수를 호출하면 상황이 완전히 달라진다. I/O 콜백은 항상 이벤트 루프의 폴(poll) 단계에서 실행된다. 폴 단계의 실행이 끝나면 이벤트 루프는 다음 단계인 확인(check) 단계로 넘어가도록 설계되어 있다. 따라서 I/O 콜백 내부에서 두 함수가 예약되면, 타이머 단계로 돌아가기 전에 확인 단계가 먼저 도래하므로 성능이나 지연 시간과 무관하게 setImmediate 콜백이 항상 먼저 실행된다는 것이 문맥상 보장된다.
이벤트 루프 지연 시간 측정하기
서버에 블로킹 코드가 포함되어 이벤트 루프가 정체되면 겉으로는 트래픽이 몰려 처리 속도가 느려진 것과 구별하기 어렵다. 이를 정확히 진단하기 위해서는 런타임 내부의 지연을 수치화해야 한다. Node.js 표준 모듈인 perf_hooks는 성능 측정 도구를 제공하며, 그중 monitorEventLoopDelay 함수는 이벤트 루프의 건강 상태를 추적하는 가장 직관적인 수단이다.
이 함수는 내부적으로 메인 스레드와 분리된 별도의 C++ 백그라운드 스레드나 운영체제 타이머 훅을 이용하여 동작한다. 지정된 해상도(resolution, 기본값 10밀리초)마다 이벤트 루프에 타이머를 예약한다. 정상적인 상황이라면 10밀리초 뒤에 정확히 콜백이 실행되어야 한다.
하지만 거대한 JSON 파싱 작업 등으로 인해 메인 스레드가 40밀리초 동안 블로킹 상태에 빠졌다면, 10밀리초 시점에 실행되어야 할 타이머 콜백은 앞선 작업이 끝난 40밀리초 시점에야 비로소 실행 기회를 얻는다. 측정 도구는 예상 실행 시점과 실제 실행 시점의 차이인 30밀리초를 '지연 시간(delay)'으로 기록한다. 이 지연 시간 데이터를 누적하여 평균(mean), 최대값(max), 백분위수 등 히스토그램 통계를 제공하므로, 개발자는 병목 구간이 시스템에 미치는 영향을 정량적으로 파악할 수 있다.
완성 코드
server.mjs
import { createServer } from 'node:http';
import { monitorEventLoopDelay } from 'node:perf_hooks';
// 10밀리초 간격으로 샘플링하는 지연 시간 측정기 생성
const histogram = monitorEventLoopDelay({ resolution: 10 });
histogram.enable();
const server = createServer((req, res) => {
// 로드 밸런서가 호출할 상태 확인 엔드포인트
if (req.method === 'GET' && req.url === '/health') {
const meanDelay = histogram.mean / 1e6;
const maxDelay = histogram.max / 1e6;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
status: 'ok',
meanDelayMs: Number(meanDelay.toFixed(2)),
maxDelayMs: Number(maxDelay.toFixed(2))
}));
return;
}
// 로그 수집 엔드포인트
if (req.method === 'POST' && req.url === '/logs') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const logData = JSON.parse(body);
// 의도적인 블로킹 시뮬레이션: heavy 타입일 경우 150ms 대기
if (logData.type === 'heavy') {
const start = Date.now();
while (Date.now() - start < 150) {
// 빈 루프로 메인 스레드 연산 점유
}
}
// 정상적인 비동기 처리 흐름으로 가정하고 응답 반환
setImmediate(() => {
res.writeHead(201, { 'Content-Type': 'text/plain' });
res.end('Log processed\n');
});
} catch (err) {
res.writeHead(400, { 'Content-Type': 'text/plain' });
res.end('Invalid format\n');
}
});
return;
}
res.writeHead(404);
res.end('Not found\n');
});
server.listen(3000, () => {
console.log('Server is running on port 3000');
// 5초마다 지연 시간 통계를 출력하고 누적 데이터 초기화
setInterval(() => {
const mean = (histogram.mean / 1e6).toFixed(2);
const max = (histogram.max / 1e6).toFixed(2);
console.log(`[Metric] Event Loop Delay - Mean: ${mean}ms, Max: ${max}ms`);
histogram.reset();
}, 5000).unref();
});
줄별 해설
const histogram = monitorEventLoopDelay({ resolution: 10 }); 구문은 내부적으로 10밀리초마다 메인 스레드의 응답성을 확인하는 객체를 생성한다. 객체가 생성된 직후 histogram.enable();을 호출해야 백그라운드 샘플링 타이머가 가동되기 시작한다.
상태 확인(health) 요청 처리부에서는 누적된 통계값을 제공한다. 히스토그램 객체가 반환하는 histogram.mean과 histogram.max 값의 단위는 나노초(nanoseconds)다. 사람이 읽기 쉽도록 1,000,000으로 나누어 밀리초(milliseconds) 단위로 변환하고 소수점 둘째 자리까지 표기한다.
로그 수집(logs) 요청 처리부에서는 POST 본문 데이터를 청크(chunk) 단위로 조립한다. 문자열 결합이 완료된 후 JSON 객체로 파싱하며, 만약 페이로드에 type === 'heavy' 속성이 존재하면 의도적으로 빈 while 루프를 150밀리초 동안 순회한다. 이 코드는 복잡한 정규식이나 거대한 JSON 파싱이 메인 스레드를 멈추게 하는 상황을 완벽하게 재현한다. 루프가 도는 동안 운영체제 수준에서 새로운 연결은 들어오지만 자바스크립트는 이를 인지하지 못하고 정지한다.
정상 응답을 반환할 때는 setImmediate로 콜백을 한 단계 미룬다. 이는 클라이언트 응답 처리를 이벤트 루프의 확인 단계로 넘겨, 대기 중이던 다른 I/O 작업(예: 다른 사용자의 파일 업로드 청크 수신)이 폴 단계에서 공평하게 실행될 기회를 제공하기 위함이다.
마지막 주기적 로깅 구문의 setInterval 끝에 붙은 unref() 메서드 호출은 매우 중요하다. 타이머가 등록되면 Node.js는 실행해야 할 작업이 남아있다고 판단하여 프로세스를 종료하지 않는다. 타이머 객체에 unref()를 호출하면 이 타이머는 참조 카운트에서 제외된다. 즉, HTTP 서버가 닫히고 처리할 연결이 없다면 이 지연 시간 로깅 타이머만 남아있더라도 프로세스는 자연스럽게 정상 종료된다.
실행 결과
터미널 하나에서 서버를 실행하고 다른 터미널에서 HTTP 요청을 보내 테스트한다. 아무런 부하가 없을 때 상태 확인 엔드포인트를 호출하면 지연 시간이 0에 가깝게 나타난다.
$ curl http://localhost:3000/health
{"status":"ok","meanDelayMs":0.15,"maxDelayMs":1.2}
무거운 연산을 유발하는 JSON 페이로드를 전송하여 메인 스레드를 블로킹 상태로 만든다. 이 요청은 처리되는 데 150밀리초가 걸린다.
$ curl -X POST -H "Content-Type: application/json" -d '{"type":"heavy"}' http://localhost:3000/logs
Log processed
무거운 요청 직후 또는 5초가 지나기 전에 상태 확인 엔드포인트를 다시 호출해 보면, 최대 지연 시간이 150밀리초 부근으로 치솟은 것을 관찰할 수 있다. 이는 이전 요청의 동기 루프가 이벤트 루프를 막아 다른 큐의 타이머 측정을 지연시켰음을 증명한다.
$ curl http://localhost:3000/health
{"status":"ok","meanDelayMs":25.4,"maxDelayMs":152.3}
서버가 실행 중인 터미널의 표준 출력(stdout)에도 5초마다 갱신된 지연 시간 측정치가 다음과 같이 로깅된다.
Server is running on port 3000
[Metric] Event Loop Delay - Mean: 0.12ms, Max: 1.05ms
[Metric] Event Loop Delay - Mean: 25.40ms, Max: 152.30ms
[Metric] Event Loop Delay - Mean: 0.14ms, Max: 0.89ms
실무에서 자주 틀리는 것
동기 코드를 Promise 안에 넣어 비동기라 착각하기
개발자들은 종종 CPU 연산이 많은 반복문을 Promise로 감싸면 백그라운드 스레드에서 비동기적으로 실행된다고 오해한다. 잘못된 코드에서는 아래와 같이 작성한다.
// 틀린 코드: 메인 스레드를 그대로 블로킹한다
function processHeavyData() {
return new Promise((resolve) => {
let sum = 0;
for (let i = 0; i < 1000000000; i++) {
sum += i;
}
resolve(sum);
});
}
Promise 객체는 비동기 작업의 결과를 담는 그릇일 뿐, 콜백 함수 자체는 호출 즉시 동기적으로 실행된다. 루프 연산이 끝날 때까지 이벤트 루프는 다음 단계로 넘어가지 못한다. CPU 집약적인 작업은 이어지는 장에서 다룰 워커 스레드(worker threads)로 완전히 분리해야 한다.
// 고친 코드: 연산은 별도 스레드로 보내고 Promise로 결과를 대기한다
function processHeavyData() {
return new Promise((resolve, reject) => {
const worker = new Worker('./heavy-worker.mjs');
worker.on('message', resolve);
worker.on('error', reject);
});
}
process.nextTick으로 재귀 호출하여 루프 정지시키기
큰 배열의 데이터를 쪼개어 처리할 때, 메인 스레드를 멈추지 않으려고 process.nextTick을 통해 다음 작업 단위를 예약하는 경우가 있다.
// 틀린 코드: 이벤트 루프 기아 상태를 유발한다
function processArrayNextTick(arr, index) {
if (index >= arr.length) return;
console.log(arr[index]);
process.nextTick(() => {
processArrayNextTick(arr, index + 1);
});
}
앞서 설명했듯 마이크로태스크 큐는 완전히 비워질 때까지 반복해서 실행된다. nextTick 내부에서 다시 nextTick을 부르면 엔진은 이 큐에서 빠져나오지 못한다. 결국 I/O 이벤트를 처리하는 폴 단계나 타이머 단계로 영영 진입하지 못해 외부 요청 처리가 먹통이 된다. 작업을 큐 맨 뒤로 양보하려면 확인 단계를 이용하는 setImmediate를 써야 한다.
// 고친 코드: 매크로태스크 큐를 이용해 다른 I/O 작업과 공평하게 스케줄링한다
function processArrayImmediate(arr, index) {
if (index >= arr.length) return;
console.log(arr[index]);
setImmediate(() => {
processArrayImmediate(arr, index + 1);
});
}
지연 시간 0인 setTimeout을 setImmediate 대신 사용하기
프론트엔드 브라우저 환경에서는 콜백을 미룰 때 setTimeout(fn, 0)을 자주 사용한다. 이를 백엔드에도 그대로 가져와 사용하는 경우가 흔하다.
// 틀린 코드: 타이머 관리를 위한 오버헤드가 발생한다
setTimeout(() => {
// 다음 틱에서 실행할 작업
}, 0);
Node.js 환경에서 지연 시간이 0인 타이머는 1밀리초로 보정될 뿐만 아니라, 타이머 최소 힙(min-heap) 자료구조에 데이터를 삽입하고 삭제하는 연산 비용을 발생시킨다. 단순히 현재 작업 이후 가장 빠른 틱에 콜백을 실행하고 싶다면, 내부적으로 링크드 리스트(linked list) 큐를 사용하여 삽입 삭제가 매우 빠른 setImmediate를 사용하는 것이 성능상 유리하다.
// 고친 코드: 확인 단계를 전용으로 사용하여 더 가볍고 빠르게 예약한다
setImmediate(() => {
// 다음 틱의 확인 단계에서 실행할 작업
});
한눈에 보기
| 단계 이름 | 주요 역할 | 관련 함수 및 이벤트 |
|---|---|---|
| 타이머 (timers) | 예약된 시간이 경과한 타이머 콜백 실행 | setTimeout, setInterval |
| 폴 (poll) | 새로운 I/O 이벤트 대기 및 콜백 실행 | fs, http, 스트림 데이터 수신 |
| 확인 (check) | 폴 단계 직후 명시적으로 지연된 작업 실행 | setImmediate |
| 닫기 (close) | 연결 종료 및 리소스 반환 마무리 작업 | socket.on('close') |
| 실행 순위 | 분류 | 함수 API | 동작 특징 |
|---|---|---|---|
| 1순위 (최상) | 마이크로태스크 | process.nextTick | 현재 실행 중인 컨텍스트가 끝나는 즉시 개입 |
| 2순위 | 마이크로태스크 | Promise.then | nextTick 큐가 비워진 후 실행 |
| 3순위 | 매크로태스크 | setTimeout, setImmediate | 이벤트 루프의 각 지정된 단계에 도달할 때 실행 |
연습 문제
- 이벤트 루프의 6가지 단계 중에서 데이터베이스 쿼리의 결과 수신이나 HTTP 응답 도착과 같은 네트워크 I/O 콜백이 실행되는 단계의 이름은 무엇인가?
- 동일한 스코프에서 process.nextTick과 Promise.resolve().then을 동시에 등록했다. 어떤 콜백이 먼저 실행되는지 마이크로태스크 큐의 동작 원리를 바탕으로 서술하시오.
- 파일 시스템의 readFile 메서드 콜백 안에서 setTimeout(fn, 0)과 setImmediate(fn)을 연속으로 호출했다. 이벤트 루프의 단계를 고려할 때 어떤 함수가 먼저 실행됨이 보장되는가?
정답과 해설
1. 정답: 폴(poll) 단계. 시스템 커널이 비동기 I/O 작업의 완료를 알리면 libuv는 이벤트 루프의 폴 단계 큐에 해당 콜백을 등록하고 실행한다.
2. 정답: process.nextTick의 콜백이 항상 먼저 실행된다. Node.js 내부 규칙상 마이크로태스크 중에서도 nextTick 큐가 Promise 큐보다 우선순위가 높기 때문이다. nextTick 큐가 완전히 비워진 후에야 Promise 기반의 콜백 검사를 시작한다.
3. 정답: setImmediate 콜백이 항상 먼저 실행된다. readFile의 콜백은 I/O 작업이므로 이벤트 루프의 폴 단계에서 실행된다. 폴 단계의 처리가 끝나면 루프는 다음 순서인 확인(check) 단계로 이동한다. 따라서 타이머 큐로 돌아가기 전에 확인 단계 전용 큐에 등록된 setImmediate 콜백이 확실하게 먼저 실행됨이 보장된다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.