"V8 은 네 코드를 돌려. libuv 는 네 코드 *사이의* 모든 걸 돌려 — 기다림, 감시, I/O. Event loop 는 libuv 의 시계야."
들어본 적 없는데 항상 쓰던 라이브러리
libuv 는 한 가지 일을 정말 잘하는 C 라이브러리야: 크로스 플랫폼 비동기 I/O. Linux 에선 epoll, macOS 에선 kqueue, Windows 에선 IOCP — 셋 다 단일 API 뒤에 숨겨. Ryan Dahl 이 Node 만들 때 async I/O 레이어를 직접 안 짰어 — libuv (원래는 libev + libeio, 나중에 통합) 를 골라서 JavaScript 로 감쌌어.
트릭: libuv 는 I/O 작업을 시작 만 시키고 ("이 1GB 파일 읽어") 바로 네 JS 코드로 돌아가게 해. 파일이 준비되면 libuv 가 V8 을 찔러서 "지금 이 callback 불러" 라고 해. 비동기 모델 전체가 한 문장이야. 나머지 — promise, async/await, stream — 다 "I/O 시작, 나중에 callback" 위에 얹은 설탕이야.
Event Loop 의 여섯 단계
libuv event loop 는 매 tick 마다 여섯 단계를 도는 deterministic 한 state machine 이야:
- Timers — threshold 지난
setTimeout/setInterval의 callback 실행. - Pending callbacks — 이전 iteration 에서 미뤄진 I/O callback 실행.
- Idle, prepare — 내부 전용; libuv housekeeping.
- Poll — 여기서 block 해서 I/O 이벤트 기다림; 도착하면 callback 실행.
- Check —
setImmediate의 callback 실행. - Close callbacks —
socket.on('close')같은 close handler 실행.
각 단계 사이 — 그리고 단계 안 각 callback 사이 — Node 가 마이크로 큐 둘을 비워: process.nextTick 먼저, 그 다음 Promise microtask. 그래서 process.nextTick(...) 가 항상 I/O 보다 먼저 돌고, await 한 promise 가 다음 tick 까지 기다리지 않고 현재 동기 블록 직후 "즉시" 재개되는 거야.
setImmediate vs setTimeout(fn, 0) vs process.nextTick
setTimeout(fn, 0)→ Timers 단계 (실제로는 최소 1ms 후).setImmediate(fn)→ Check 단계 (Poll 다음).process.nextTick(fn)→ 마이크로 큐, 다음 단계 시작 전에 실행.
queueMicrotask (Promise microtask) 아니면 setImmediate 야. process.nextTick 은 잘못 쓰면 I/O 굶겨.아무도 얘기 안 하는 thread pool
"Node 는 single-threaded" 는 반쪽 진실이야. 네 JavaScript 가 single-threaded 로 돌아 — V8 instance 가 딱 하나, 메인 스레드 하나. 근데 libuv 가 OS 가 async API 안 주는 작업들 위해 thread pool 을 굴려 (기본 4 개, UV_THREADPOOL_SIZE 로 설정 가능): 대부분 플랫폼에서 파일 시스템 I/O, getaddrinfo 통한 DNS lookup, 일부 crypto 작업.
그래서 Node 앱한테 동시 readFile 호출 두들기면 결국 느려져 — V8 이 아니라 4-thread libuv pool 을 포화시키는 거야. production 에서 UV_THREADPOOL_SIZE=16 으로 올리는 건 진짜 레버야; 대부분 존재 자체를 몰라.