티스토리 뷰
브라우저 VS Node.js의 이벤트 루프 차이
브라우저 이벤트 루프(Event Loop)란
브라우저의 이벤트 루프에 대해서는 지난글에 다뤘으니, 링크만 걸어두겠다.
Q. 브라우저 이벤트 루프 관련해서만 요약해줘
A. 해당 블로그 글에서는 자바스크립트의 이벤트 루프와 비동기 처리, 그리고 가비지 컬렉션 간의 관계를 다루고 있다.
주요 내용을 요약하면 다음과 같다.
- 이벤트 루프(Event Loop)
- 자바스크립트는 싱글 스레드 기반으로 한 번에 하나의 작업만 실행할 수 있다.
- 하지만 비동기 작업(예: HTTP 요청, setTimeout)을 처리하기 위해 이벤트 루프 메커니즘을 사용한다.
- 이벤트 루프는 콜 스택(Call Stack), 태스크 큐(Task Queue), 마이크로태스크 큐(Microtask Queue)로 구성된다.
- 콜 스택(Call Stack)
- 동기적인 작업(함수 호출, 연산 등)을 처리하며, 스택 자료구조로 선입후출(LIFO) 방식으로 동작한다.
- 태스크 큐(Task Queue)
- 비동기 작업의 콜백 함수(setTimeout, setInterval 등)가 대기하는 곳으로, 큐 자료구조로 선입선출(FIFO) 방식으로 동작한다.
- 콜 스택이 비어있을 때 이벤트 루프가 태스크 큐에서 작업을 가져와 실행한다.
- 작업 단위를 Miarotask라고 한다.
- 이벤트 루프에서 한 틱(tick)에 하나씩 실행되는 비동기 작업 단위
- 일반적인 비동기 함수 콜백들이 여기 들어간다.
- 마이크로태스크 큐(Microtask Queue)
- Promise의 then, catch와 같은 콜백이 대기하는 곳으로, 태스크 큐보다 우선순위가 높다.
- 콜 스택이 비어있을 때 이벤트 루프는 먼저 마이크로태스크 큐를 확인하여 작업을 실행한다.
- 작업 단위를 Microtask라고 한다.
- 빠른 후속 처리, 상태 동기화, React 내부 처리 등이 여기 들어간다.
이러한 메커니즘을 이해하면 자바스크립트의 비동기 처리와 메모리 관리 방식을 효과적으로 활용할 수 있다.
브라우저 이벤트 루프(Event Loop)란
자바스크립트는 싱글스레드이지만, 비동기 작업을 처리하기 위해서 이벤트 루프가 루틴대로 큐를 돌면서 태스크를 하나씩 처리하는 구조를 가졌다.
구성
- 콜 스택 (Call Stack)
- 웹 APIs (Web APIs (setTimeout, fetch, DOM events...))
- 태스크 큐 (Task Queue)
- 마이크로 태스크 큐 (Microtask Queue (Promise, queueMicrotask))
- 렌더링 스텝 (Rendering Step)
- DOM 변경 사항을 실제 화면에 반영하는 타이밍
실행 흐름
이벤트 루프는 콜 스택이 비워져 있으면 태스크 큐나 마이크로 태스크큐에서 실행할 것을 꺼내오게 되는데,
이때 순서는 태스크 큐를 한번 비우고 마이크로 태스크 큐의 전체를 비우는 순으로 이루어진다.
브라우저는 마이크로태스크큐의 우선순위가 높지만, 마이크로 태스크큐를 먼저 비우지는 않는다. (루프의 전체 관점이 아니라 Mactorask 1개 실행 직후 관점에서 봐야한다.)
흐름
과정(이벤트 루프 tick)을 한 프레임 내에서 반복하고 필요시 렌더링을 트리거한다.
- Call stack 비었는지? → 비었다.
- Task Queue에서 Macrotask 하나 꺼내서 실행
- 그 Macrotask가 끝났으면 → Microtask Queue 전부 실행
- 그 다음 → requestAnimationFrame 콜백 실행 (렌더링 타이밍이면)
- 그 다음 → 렌더링 (if 타이밍 맞으면)
- 다음 루프
Q. '과정(이벤트 루프 tick)을 한 프레임 내에서 반복하고 필요시 렌더링을 트리거한다.' 프레임과 틱 설명
A. 프레임과 이벤트 루프의 틱(tick)은 비슷해 보이지만 다른 개념이다.
한 프레임 안에서 여러 이벤트 루프 틱이 발생할 수 있는 구조이다.
설명
- 이벤트 루프 Tick
- 이벤트 루프가 한 번 돌면서 macrotask > microtask > 렌더링 기회 > 다음 macrotask 과정을 반복하는 실행 단위이다.
- 프레임
- 브라우저가 화면을 그리는 주기 (기본적으로 1초에 최대 60번, 즉 16.66ms마다)
즉, 여러 Tick이 모여서 하나의 프레임이 될 수 있다.
- 🌀 이벤트 루프:
- [Tick 1] Macrotask → Microtask
- [Tick 2] Macrotask → Microtask (시간 지남...)
- ⏳ 16ms 지남 → 렌더링 타이밍 도달 [브라우저] → requestAnimationFrame 콜백 실행 → 화면 렌더링 발생 (paint, layout 등)
'여러 이벤트 루프 틱이 한 프레임 내에서 실행될 수 있고, 브라우저는 프레임 타이밍이 도달하면 그때서야 렌더링을 한다' 라는 뜻
이벤트 루프 예시
setTimeout(() => console.log('timeout 1'), 0);
Promise.resolve().then(() => console.log('promise 1'));
setTimeout(() => console.log('timeout 2'), 0);
Promise.resolve().then(() => console.log('promise 2'));
최종 출력
- promise 1
- promise 2
- timeout 1
- timeout 2
실행 흐름
- setTimeout 2개 Task Queue에 추가, Promise.then 2개 Microtask Queue에 추가
- Call Stack이 비어있으니, 이벤트 루프가 Task Queue와 Microtask Queue에서 실핼할 태스크 꺼내오게됨
- 이때 Task Queue에서 먼저 한개의 태스크를 꺼내오게된다.
- 하지만 setTimeout이 0ms이지만 최소 지연 시간(1ms~4ms)으로 인해 Task Queue에서 꺼낼 태스크가 없다.
- 따라서 Microtask Queue에서 태스크를 가져오게 되는데, 특성상 전체를 비우게 되어있기 때문에 Promise1 과 Promise2가 모두 실행된다.
- Microtask Queue가 다 비워지면, 다시 이벤트루프 틱이 돈다.
- Task Queue에서 1개의 태스크를 가져오는데, 이제 최소 지연 시간이 지난 timeout1 하나 실행된다.
- 이후 다시 Microtask Queue 다시 확인하는데 전부 다 비워져있기에 다시 Task Queue에서 1개의 태스크를 가져오게되어 timeout2이 실행된다.
이벤트 루프 1회 사이클
- 콜스택이 비었는지 체크 → Task Queue에서 Macrotask 1개 꺼내 콜스택에 넣고 실행 → 실행 중에 콜스택이 채워졌다가 다시 비게됨 → Macrotask 끝나면, Microtask Queue 전부 실행 → Microtask 끝나면, 필요할 경우 렌더링 → 다음 루프 (다시 1번)
Q. 위에서 설명한 최소 지연 시간이, setTimeout(fn, 0)인데 발생하는게 맞아?
A. 맞다.
브라우저에 의해 스케줄링 된 후 최소지연시간 (1ms~4ms)이후에 Task Queue에 들어가게 되기에 setTimeout(fn,0ms)여도 바로 실행되지 않는다.
HTML 스펙 상 브라우저는 최소 지연 시간을 1ms~4ms이상 보장해야하기 때문이다.
물론 최신 브라우저의 스펙 상 1ms여도 항상 setTimeout은 최소한 다음 루프 tick에서만 실행이 가능하다.
(최소 지연 시간이 0.5ms, 0.1ms로 빨라져도 HTML 스펙의 정의 상, '지연이 끝난 후 다음 루프 사이클에서'라고 되어있기에 항상 다음 루프에서 실행된다고....)
지연 시간 동안에는 WebAPIs 영역에서 대기하고 타이머가 지난 후 Task Queue에 진입하기 때문이다.
반면 Promise.then이나 queueMicrotask는 지연없이 바로 현재 루프의 Microtask Queue에 들어가기때문에 setTimeout(fn, 0)보다 먼저 실행되는 경우가 발생한다.
queueMicrotask(callback) 란?
ES2020에 추가된, microtask를 등록하는 공식 API이다.
브라우저, Node.js 둘 다 지원하고 Promise.then과 같은 microtask의 대안이다.
즉, 이벤트 루프에서 microtask queue에 콜백을 직접 등록하는 함수이다.
Promise.then 대신 사용하며 불필요한 Promise 객체 생성을 피할수 있고(성능상 미세하지만 의미있다고하는데..), 명시적으로 사용하기 위해 추가되었다고 한다.
보통 DOM 업데이트 끝난 직후 다음 이벤트 루프(tick)가 돌기 전, 정밀한 타이밍에 실행되기에 이때 필요한 추가할 작업이 있을때 사용된다. (레이아웃 측정, snapshot 찍기, transition 타이밍 조절 등)
주의할 점은 예외처리를 위해 꼭 try/catch를 사용하라고 한다.
차이점
메서드 | 큐 | 설명 |
setTimeout(fn) | 🧱Task Queue (Macrotask) | 최소 4ms~ 지연, 다음 이벤트 루프에 실행됨 |
Promise.then(fn) | ⚡ Microtask Queue | 현재 콜스택 후, 바로 실행 |
queueMicrotask(fn) | ⚡ Microtask Queue | Promise.then()과 동일하게 실행됨, 더 명시적임 |
예시
console.log('start');
queueMicrotask(() => {
console.log('microtask');
});
setTimeout(() => {
console.log('macrotask');
}, 0);
console.log('end');
출력
- start
- end
- microtask
- macrotask
예시)
미세하지만 의미있는 경우 - 1,000,000번 Microtask 등록 성능 비교
- Promise.resolve().then은 매 호출마다 Promise 객체를 생성한다.
- queueMicrotask()은 불필요한 객체 생성 없이 Microtask만 등록한다.
console.time('Promise.then');
for (let i = 0; i < 1_000_000; i++) {
Promise.resolve().then(() => {});
}
console.timeEnd('Promise.then');
console.time('queueMicrotask');
for (let i = 0; i < 1_000_000; i++) {
queueMicrotask(() => {});
}
console.timeEnd('queueMicrotask');
브라우저마다 차이는 있지만,
대량 실행 시에 대부분의 경우 보통 queueMicrotask 쪽이 더 빠르고 메모리 소비도 적다.
마이크로태스크를 대량으로 등록해야 할 때나 실시간성, 이벤트 루프 제어가 중요한 프레임워크, 런타임 레벨(예: React Fiber, Vue core 등)에서 사용할 경우 큰 의미가 있다.
예시) 크롬 기준 항목걸린 시간 (출처: 챗지피티)
항목 | 걸린 시간 |
Promise.then | 약 90~120ms |
queueMicrotask | 약 30~50ms |
예시)
React(18+) 내부에서 queueMicrotask가 쓰이는 실제 예시 (ReactDOM - scheduler 패키지 내부)
- React는 useEffect, useLayoutEffect, Suspense, transition 등에서 비동기 타이밍을 미세하게 제어하기 위해 queueMicrotask를 사용한다.
- React Fiber에서 'DOM 반영이 완료된 직후, 다음 루프 전' 이라는 아주 민감한 타이밍을 보장하기 때문에 flushSync, Suspense, transition, useEffect 타이밍 제어에 핵심적으로 쓰인다.
scheduleMicrotask.js
let scheduleMicrotask;
if (typeof queueMicrotask === 'function') {
scheduleMicrotask = queueMicrotask;
} else {
scheduleMicrotask = (callback) => {
Promise.resolve().then(callback);
};
}
queueMicrotask는 프레임워크 수준에서 아주 정밀한 이벤트 루프 타이밍 제어를 할 수 있기에 React 18+에서는 거의 표준처럼 쓰이고 있다.
Q. queueMicrotask vs nextTick vs Promise.then 차이
A.
요약
항목 | queueMicrotask | Promise.then | process.nextTick |
실행 시점 | 현재 콜스택 후, microtask queue | 현재 콜스택 후, microtask queue | 현재 콜스택 후,nextTick queue |
큐 종류 | Microtask Queue | Microtask Queue | Next Tick Queue (Node 전용) |
표준 여부 | ✅ 웹 표준 (ES2020) | ✅ 웹 표준 | ❌ Node.js 전용 API |
브라우저 사용 | ✅ 지원 | ✅ 지원 | ❌ 불가 |
Node.js 사용 | ✅ 지원 (v11+) | ✅ 지원 | ✅ 지원 |
성능 | ✅ 빠름 (Promise 객체 없음) | ❌ 상대적으로 느림 (Promise 생성 필요) | ⚠️ 너무 빠름 (스타 벗어남 주의) |
예외 처리 | ❗ try/catch 필요 | 내부 catch로 가능 | ❗ 예외 시 프로세스 중단 |
실행 우선순위 (Node.js 기준)
- process.nextTick > queueMicrotask ≒ Promise.then > setTimeout
실행 우선순위 (브라우저 기준)
- queueMicrotask ≒ Promise.then > requestAnimationFrame > setTimeout
Q. Rendering Step이 뭐야?
A. 브라우저가 화면을 그리는 단계를 말한다.
Rendering Step은 브라우저가 DOM/CSS 변경사항을 실제 화면에 반영하는 시점을 말한다.
모든 Task와 Microtask가 끝난 뒤 실행되며, 1 프레임당 최대 한 번 발생한다.
대부분의 브라우저는 60fps 기준 → 약 16.6ms마다 한 번 렌더링되며, 프레임을 넘기면 "프레임 드랍" 발생하여 사용자 경험에 영향을 준다. (렉 걸리는 느낌, 버벅임)
흐름
- Macrotask 처리 (예: setTimeout, fetch, 이벤트 핸들러 등)
- Microtask 처리 (예: Promise.then, queueMicrotask)
- ✅ Rendering Step ← 여기서 렌더링 발생!
- 다음 Macrotask 처리로 루프 반복
즉, Macrotask → Microtask → Rendering 순서로 계속 반복되는 구조이다.
처리 작업
렌더링 스탭은 브라우저의 렌더링 엔진이 관여하는 단계이고 아래 작업들을 처리한다.
작업 | 설명 |
스타일 계산 (Style) | CSS 계산 (color, margin 등) |
레이아웃 계산 (Layout) | 각 요소의 위치와 크기 계산 |
페인트(Paint) | 실제 픽셀로 그릴 정보 준비 |
합성(Composite) | 여러 레이어를 합쳐서 하나의 화면으로 그림 |
디스플레이(Display) | 사용자 화면에 출력 |
이 과정을 통틀어 Reflow + Repaint + Composite 라고 한다.
예시
document.body.style.background = 'red';
Promise.resolve().then(() => { // 이 시점까지도 화면은 아직 안 바뀜 });
브라우저는 DOM/CSSOM 변경을 바로 렌더링 하지 않고, Microtask Queue 까지 전부 비운 다음에 Rendering Step에서 한번에 그려준다. (불필요한 리플로우/리페인트를 줄여 성능 최적화의 목적)
관련된 API
API | 관련성 |
requestAnimationFrame() | Rendering Step 직전에 콜백 실행됨 → 렌더 최적화에 사용 |
MutationObserver | DOM 변경 감지 → Microtask로 실행됨, 렌더 전에 트리거됨 |
ResizeObserver | Layout 계산 이후에 트리거됨 |
Node.js 이벤트 루프 구조
브라우저의 이벤트 루프와 구조는 비슷하지만, 훨씬 세분화된 단계(Phase)로 이루어져 있고 Node.js만의 특성도 있다.
자바스크립트를 싱글 스레드 기반 비동기 I/O 처리 환경에서 실행할 수 있도록 이벤트루프를 기반으로 동작한다.
이 이벤트 루프는 libuv라는 C++ 라이브러리가 관리하고, 6가지 단계(Phase)로 구성되어있다.
Node.js 이벤트 루프는 여러 단계로 나눠져 있고, 각 단계에서 특정 종류의 작업만 처리한다.
Q. '이 이벤트 루프는 libuv라는 C++ 라이브러리가 관리' 한다는게 어떤 의미?
A. 말그대로 Node.js 내부의 C++ 기반 라이브러리(libuv)가 처리한다는 의미이다.
Node.js에서는 fs.readFile, setTimeout, http.get과 같은 비동기 작업을 쉽게 사용할 수 있는데,
그 이유는 libuv가 C++로 동작하면서 백그라운드에서 I/O, 타이머, 네트워크 작업을 따로 처리해주고 있기 때문이다.
즉, 자바스크립트는 '작업 요청'만 하고, libuv가 '언제 실행할지'를 결정하는 이벤트 루프를 실제로 관리한다.
libuv는 뭘까?
libuv는 이벤트 루프를 기반으로 비동기 I/O를 지원하는 다중 플랫폼(Windows, Linux, macOS) C 라이브러리이다.
Node.js 내부에서 이벤트 루프, 타이머, I/O 처리를 담당하고 있으며, 비동기/논블로킹 시스템을 만들어주는 핵심 엔진이다.
기능 | 어떻게 처리되는가 |
파일 읽기 (fs.readFile) | libuv가 OS에 비동기 파일 읽기 요청 → 완료되면 JS 콜백 호출 |
타이머 (setTimeout) | libuv가 타이머를 등록 → 만료되면 콜백 실행 |
네트워크 요청 | libuv가 소켓 연결 관리 → 데이터 도착하면 콜백 호출 |
논블로킹 처리 | JS는 기다리지 않고 다음 코드 실행 → 처리 완료되면 콜백으로 알림 |
흐름
JS Engine ← 우리가 쓰는 JS 코드 (V8)
l
libuv ← C++로 구현된 이벤트 루프, I/O 처리
l
파일 시스템, 네트워크, 타이머 등 OS 리소스에 접근
예시)
fs.readFile('data.txt', (err, data) => {
console.log(data.toString());
});
- 자바스크립트는 fs.readFile()이라는 비동기 요청만 던진다.
- 실제 파일을 읽는 건 libuv가 담당한다.
- 파일이 준비되면 콜백을 이벤트 루프에 등록한다.
- 콜 스택이 비게되면 콜백 실행된다.
이벤트 루프 6단계
- 타이머 단계 (timers): setTimeout, setInterval 콜백 실행
- 대기 콜백 단계 (pending callbacks): 시스템 내부에서 예약된 I/O 관련 콜백 처리
- 유휴/준비 단계 (idle, prepare): libuv 내부용 단계 (일반 사용자 코드 없음)
- 폴링 단계 (poll): I/O 이벤트를 기다리고, 도착한 이벤트의 콜백 실행
- 체크 단계 (check): setImmediate 콜백이 실행되는 단계
- 종료 콜백 단계 (close callbacks): socket.on('close'), process.on('exit')등 처리
- 네트워크 연결, 파일 핸들 등의 리스소가 정리될때 (run-time cleanup) 호출되는 콜백들이 여기서 처리된다.
- 일반적인 비동기 로직(fs.readFile, setTimeout, Promise)은 해당하지 않는다.
- process.on('exit')은 이벤트 루프의 흐름과는 별개로 작동한다. 모든 작업이 끝난 뒤 마지막 cleanup 시점에 한번 호출, 안에서 비동기작업을 하면 무시된다.
각 단계 사이에 Microtask Queue(Promise.then, process.nextTick)가 처리된다.
Mictotask Queue
Node.js에는 2가지 종류의 Microtask가 있다.
종류 | 우선순위 | 설명 |
process.nextTick() | 1순위 | 이벤트 루프 단계 진입 전에 항상 먼저 실행 |
Promise.then() | 2순위 | 각 phase 이후 처리되는 일반 Microtask |
순서 요약
- Call Stack clear
- process.nextTick Queue 처리
- Microtask(Promise.then) Queue 처리
- 다음 Phase로 이동
실행 순서 예시)
setTimeout(() => {
console.log('setTimeout')
}, 0);
setImmediate(() => {
console.log('setImmediate')
});
Promise.resolve().then(() => {
console.log('promise')
});
process.nextTick(() => {
console.log('nextTick')
});
console.log('sync');
예상 출력 순서
- sync: 콜 스택을 가장 먼저 비운다.
- nextTick: 이벤트 루프 단계 진입 전에 항상 먼저 실행
- promise
- setTimeout or setImmediate (상황에 따라 순서 바뀜): poll 단계가 비었는지에 따라 다름
예시)
console.log('🧵 start');
process.nextTick(() => {
console.log('🌀 nextTick 1');
});
Promise.resolve().then(() => {
console.log('✨ promise 1');
});
setTimeout(() => {
console.log('⏰ timeout');
process.nextTick(() => {
console.log('🌀 nextTick 2');
});
Promise.resolve().then(() => {
console.log('✨ promise 2');
});
}, 0);
setImmediate(() => {
console.log('🚀 immediate');
process.nextTick(() => {
console.log('🌀 nextTick 3');
});
Promise.resolve().then(() => {
console.log('✨ promise 3');
});
});
예상 출력 순서
- 🧵 start
- 🌀 nextTick 1 ← 1. main phase 끝난 직후 nextTick
- ✨ promise 1 ← 2. 그다음 microtask (main phase)
- ⏰ timeout ← 3. timers phase
- 🌀 nextTick 2 ← 4. timers phase 끝날 때
- ✨ promise 2 ← 5. nextTick 끝나고
- 🚀 immediate ← 6. check phase
- 🌀 nextTick 3 ← 7. check phase 끝날 때
- ✨ promise 3 ← 8. nextTick 끝나고
요약
Node.js는 libuv 기반으로 동작하고 6단계로 구성된 이벤트 루프를 가진다.
각 단계가 끝난후, process.nextTick()과 마이크로큐(Promise)가 순서대로 실행된다.
- 각 단계에 등록된 콜백 실행
- 콜백 실행 중 process.nextTick(), Promise.then() 같은 마이크로태스크가 추가될 수 있다.
- 그 단계안의 모든 콜백이 끝나면,
- 먼저 process.nextTick()큐를 비운다. (Node.js 전용 마이크로태스크)
- 그 다음 마이크로 큐(Promise등)을 비운다.
- 다음 단계로 이동한다.
process.nextTick()은 모든 것보다 먼저 실행된다. (너무 많이 쓰면 starvation 발생 가능)
Q. 이벤트 루프 단계는 1단계 -> 6단계로 실행되는가?
A. 아니다.
Node.js 이벤트 루프는 각 단계를 무조건 순서대로 다 돌진 않는다.
Node.js의 이벤트 루프는 한번 돌때마다 필요한 단계만 수행하고, 상황에 따라 일부 단계는 스킵하거나 빨리 지나간다.
여기서, '빨리 지나간다는건'
해당 단계에 처리할 작업(콜백, 이벤트)이 아무것도 없을 경우 해당 단계는 수행되지만, 바로 종료되고 다음 단계로 넘어간다는 것이다.
즉, 건너뛰는 개념이 아니고, 실행-종료를 수행하며 거의 0ms 소요한다는 의미이다.
정리
phase | 상태 | 처리 | 방식 설명 |
1. timers | setTimeout 없음 | ✅ 빨리 지나감 | 해당 phase는 실행되지만 처리할 콜백이 없어 바로 빠져나감 |
2. pending callbacks | 없음 | ✅ 빨리 지나감 | 시스템-level 콜백이 없어서 즉시 통과 |
3. idle, prepare | 내부 phase | ✅ 빨리 지나감 | 일반적으로 libuv 내부용이라 외부 작업 없으면 금방 지나감 |
4. poll | I/O 이벤트 없음 | ⚠️ 조건부 → 대기 또는 스킵 | |
→ | |||
(1) 타이머 예정돼 있으면 스킵하고 timers로 점프 | |||
(2) 대기 시간이 남으면 잠시 기다렸다가 넘어감 | |||
(3) 아무것도 없으면 바로 check phase로 넘어감 | |||
→ 상황 따라 달라지므로 "스킵" 또는 "빨리 지나감" 모두 될 수 있음 | |||
5. check | setImmediate 있음 | ✅ 실행됨 | 콜백 실행 |
6. close callbacks | 없음 | ✅ 빨리 지나감 | 닫힌 리소스가 없으니 바로 넘어감 |
예시)
setTimeout(() => console.log('⏰ setTimeout'), 0);
setImmediate(() => console.log('🚀 setImmediate'));
- setTimeout가 0ms되어있지만, 정확히는 최소한의 지연이 필요하다.
- 타이머 큐는 '현재시각 >= 등록시간 + 지연 시간' 조건이 맞아야 실행된다. 따라서 setTimeout(fn, 0)이라고 해도 실제 실행은 다음 timers에 진입했을때가 된다.
- 첫번째 timers에서 setTimeout이 실행되지 않기 때문에 첫 사이클의 루프에서는 timers에서 아무 작업이 없는게 되므로, '빨리 지나감'의 처리를 거쳐 poll 단계에 진입하게 된다.
- poll에서 처리하는 I/O 이벤트와 콜백이 없으므로 바로 다음 단계인 check로 넘어간다.
- check 단계에서는 setImmediate 콜백이 실행되는 단계이므로, setTimeout보다 먼저 setImmediate가 실행되게 되어 '🚀 setImmediate'가 먼저 출력된다.
- 이벤트 루프의 다음 사이클이 돌면서 timers에서 setTimeout이 실행되게 되고 '⏰ setTimeout'이 출력된다.
즉, Node.js 이벤트 루프는 단계는 정해져있지만 실제로 어떤 순서로 실행되느냐는 현재 처리할 작업에 따라 다르다.
1~6단계 순회가 아니라 '지금 실행할 일이 있는 단계만 선택적으로 수행'이며, 예시에 따라 setImmediate가 setTimeout보다 먼저 실행되는 경우도 있다.
상황 실행 | 순서 예시 |
아무 것도 없고 setImmediate()만 있음 | check phase → 바로 실행 |
setTimeout(fn, 0)만 있음 | timers phase → 바로 실행 |
둘 다 있을 경우 | 상황에 따라 setImmediate()가 먼저 실행되기도 함 (OS & Node 버전에 따라 다름) |
I/O 작업 콜백 안에서 | setTimeout이 setImmediate보다 먼저 실행되기도 함 |
예시)
Node.js 이벤트 루프의 타이밍 차이
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('⏰ setTimeout');
}, 0);
setImmediate(() => {
console.log('🚀 setImmediate');
});
});
- fs.readFile은 poll 단계에서 실행된다.
- 이 안에서 등록된 setTimeout은 timers 단계에서 실행된다. (즉, 다음 이벤트 루프 사이클)
- 반면 setImmediate는 바로 check 단계에서 등록된다. (poll의 바로 다음 단계이므로, setImmediate가 거의 항상 먼저 실행)
여기서 '거의' 항상 먼저라고 사용하는 말은 이론상이기 때문에 대부분 동작은 이렇게 되지만 무조건은 아니란 말이다.
즉, 예외가 있다는 의미이다.
poll 단계에서 대기 시간과 타이머 등록 타이밍이 겹치는 경우도 있기 때문이다.
- 예외적으로 poll 단계에서 I/O 이벤트를 기다리면서 일정 시간 대기할 수 있는데, 이때 setTimeout의 0ms가 도달하면, Node.js는 poll 단계에서 timers 단계로 점프하여 setTimeout을 먼저 처리할 수 있다. 그러면 setImmediate가 그 다음 루프의 check 단계로 밀리게 되는 것이다.
Node.js의 이벤트 루프는 브라우저 이벤트 루프와는 구조 자체가 다르며, 브라우저 이벤트 루프는 명시적인 단계보다는 루틴 순서로 돌아가는 단순한 루프 구조를 가지고 있다.
Q. 유휴/준비 단계는 libuv 내부용 단계라고 되어있는데, 간단히 설명
A. libuv 내부에서만 사용하는 특수한 단계이고 일반 자바스크립트 코드에서는 직접 이 단계에 접근하거나 뭔가 실행하지 않는다.
유휴(idle)는 CPU가 노는 시간, 입출력을 기다리는 시간 이런것을 의미하는데,
말 그대로 '유휴/준비 단계'의 역할은 이벤트 루프가 다음 I/O를 기다리기 전에 남는 시간 동안 처리할 준비 작업을(prepare)하거나 내부 연산을 수행한다.
예를 들어 내부 타이밍 계산이나 다음 Phase에 전달할 데이터 정리등을 진행한다.
일반적으로 개발자가 입장에서 알고만 있으면 되며, 직접 사용하는 것은 없다.
이 단계는 오직 libuv 엔진 내부에서만 쓰는 예약된 시간대 같은 것이며 실제로도 우리가 사용하는 타이머, I/O, setImmediate, 콜백 등은 다 다른 단계에서 처리된다.
Q. '체크 단계 (check): setImmediate 콜백이 실행되는 단계' 라고 했는데, setImmediate는 뭐고, mdn에 비권장되어있는데 관련 설명해줘
A. setImmediate는 비동기 타이밍을 제어할때 등장하는 도구 중 하나인데, 비권장하고 있다.
이벤트 루프가 끝난 뒤 즉시 실행되는 콜백을 등록하는 함수이며, Node.js에서만 사용가능하며 브라우저에는 없다.
(check 단계에서 실행된다.)
비권장 이유는 아래와 같다.
- 일관되지 않은 실행 타이밍
- poll 단계 -> check 단계에서 실행되고, setTimeout(fn, 0)은 timers 단계에서 실행된다.
- 이 두개가 겹칠 경우, 실행 순서가 운영 체제 I/O 상황에 따라 달라지므로 예측불가하고 디버깅이 어렵고, 플랫폼 종속적이된다.
- 브라우저에 없음
- 완전한 Node.js 전용 API이므로 크로스 플랫폼 호환성이 떨어진다.
- 모던 자바스크립트에서는 Promise, queueMicrotask, setTimeout으로 대체된다.
- Node.js 공식 문서에서도 비권장
- deprecated될 가능성 존재
- 대체 방법
- 이벤트 루프 이후에는 setTimeouf(fn, 0) - 조금 느리지만 확실하다.
- 비동기 로직->다음 틱에 실행에는 process.nextTick(cb) - 무조건 우선 실행되므로 많이 쓰면 stack이 밀린다.
브라우저 이벤트루프와 Node.js 이벤트 루프 차이
구분 | 브라우저 | Node.js |
비동기 큐 | Macrotask + Microtask | 6개 Phase + Microtask + nextTick |
Microtask 처리 시점 | 매 Task 뒤 | 각 Phase 뒤 |
setImmediate | 없음 (대신 setTimeout(fn, 0)) | check phase에서 실행 |
process.nextTick | 없음 | Microtask보다 우선 처리 |
렌더링 관련 처리 | 존재 (ex. requestAnimationFrame) | 없음 (렌더링 X) |
Web API | DOM, fetch 등 브라우저 제공 | Node 자체 I/O, fs, net 등 |
예시)
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('promise'));
console.log('sync');
브라우저 출력
sync promise setTimeout
Node.js 출력
sync promise setTimeout
동일해 보이지만, Node.js에서는 내부적으로 nextTick, poll/check phase 순서에 따라 동작이 다르게 조정될 수 있다.
(Node.js의 경우, 상황(특히 I/O 포함 여부)에 따라 출력이 달라질 수 있다.)
요약
- 브라우저는 렌더링 중심이며 'Macrotask 하나 처리 → Microtask 전부 처리 → 렌더링 (렌더링 타임이 맞는 경우에만) → 다음 루프'
- Node.js는 I/O 중심, 다양한 단계 → Microtask → 다음 단계
- process.nextTick()은 Node.js 전용 우선 실행 큐
- 실전에서는 setTimeout, Promise, nextTick이 어떤 순서로 실행되는지를 정확히 이해해야 디버깅/성능 튜닝이 가능하다.
'개념 > 2025 학습' 카테고리의 다른 글
프로젝트에서 Vue 쓸까? React 쓸까? (0) | 2025.04.06 |
---|---|
데이터 바인딩(Data Binding)과 MVVM (Vue, React) (0) | 2025.04.06 |
자바스크립트 쓰로틀링(Throttling), 디바운싱(Debouncing) 성능 최적화 (0) | 2025.04.06 |
가상돔(Virtual DOM)과 React Fiber 구조 (0) | 2025.03.26 |
이벤트 루프와 Web APIs의 관계 (0) | 2025.03.06 |
이벤트 위임(Event Delegation)과 성능 최적화 (0) | 2025.02.27 |
프로토타입과 프로토타입 체인 (0) | 2025.02.25 |
프로미스 체이닝 (Promise Chaining) (0) | 2025.02.12 |