Node.js의 논블로킹 I/O 완전 정복 — 이벤트 루프부터 비동기 동작까지
Node.js를 공부하다 보면 빠지지 않는 개념이 있습니다.
바로 논블로킹 I/O(Non-blocking I/O)와 이벤트 루프(Event Loop)입니다.
Node.js는 싱글 스레드임에도 불구하고 수천 개의 요청을 동시에 처리할 수 있습니다.
그 비밀이 바로 “논블로킹 I/O”에 있습니다.
🧩 1. Node.js 런타임의 I/O 모델
Node.js는 자바스크립트 엔진(V8) 위에 libuv라는 C 레벨의 런타임 계층을 얹은 구조입니다.
┌──────────────────────────┐
│ JavaScript Code (User) │
│ └─ Event Loop │
│ └─ Callback Queue │
├──────────────────────────┤
│ libuv (I/O Thread Pool) │
│ ├─ epoll / kqueue / IOCP│
│ └─ Async Workers │
├──────────────────────────┤
│ Operating System Kernel │
└──────────────────────────┘
이 구조 덕분에 Node.js는 I/O를 직접 수행하지 않고 OS에게 위임합니다.
자바스크립트 엔진은 I/O 요청만 등록해두고 즉시 다음 작업으로 넘어가죠.
⚙️ 2. 블로킹 I/O vs 논블로킹 I/O
먼저 두 가지 개념을 비교해봅시다.
| 구분 | 블로킹 I/O | 논블로킹 I/O |
| 동작 방식 | 호출한 함수가 I/O 완료까지 기다림 | 즉시 반환 후, 완료 시점에 콜백 실행 |
| CPU 활용 | 대기 중 유휴 상태 | 다른 요청 처리 가능 |
| 코드 예시 | fs.readFileSync() | fs.readFile() |
| 병렬 처리 | 불가능 | 가능 |
🧱 블로킹 I/O 예시
const fs = require("fs");
const data = fs.readFileSync("file.txt", "utf-8");
console.log("파일 내용:", data);
console.log("이 코드는 파일 읽기 후 실행됨");
동작 흐름
파일을 읽기 시작
파일 읽기 완료될 때까지 콜 스택이 멈춤
완료 후
data를 반환다음 줄 실행
⚡ 논블로킹 I/O 예시
const fs = require("fs");
fs.readFile("file.txt", "utf-8", (err, data) => {
console.log("파일 내용:", data);
});
console.log("이 코드는 먼저 실행됨");
동작 흐름
readFile()호출 → 즉시 반환파일 읽기 요청은 libuv의 I/O 스레드 풀에서 처리
읽기 완료 시 → 이벤트 루프가 콜백 큐에 등록
콜 스택이 비면 콜백 실행
출력 결과:
이 코드는 먼저 실행됨
파일 내용: (file.txt 내용)
즉, I/O 자체는 백그라운드에서 비동기적으로 수행되며,
JS 스레드는 계속해서 다음 코드를 실행할 수 있습니다.
🔄 3. 이벤트 루프(Event Loop)와 논블로킹의 연결
Node.js의 이벤트 루프는 다음과 같은 사이클로 동작합니다.
콜 스택(Call Stack) 이 비면,
큐(Queue) 안에 있는 마이크로태스크(Promise) → 매크로태스크(setTimeout 등) 순서로 콜백을 꺼냄
각 콜백을 실행하면서 새로운 I/O를 다시 libuv에 위임
이 구조 덕분에 Node.js는 동시에 수천 개의 요청을 처리할 수 있습니다.
(물론 실제 병렬은 아니고, I/O는 OS가 비동기로 수행합니다.)
💬 4. 블로킹/논블로킹, 동기/비동기 — 네 개념의 관계
이제 많은 분들이 헷갈려 하는 이 조합을 명확하게 정리해봅시다.
| 구분 | 기준 | 설명 |
| 블로킹 vs 논블로킹 | 함수 호출 시점 | 호출 즉시 반환되는가? |
| 동기 vs 비동기 | 결과 처리 시점 | 결과를 언제, 어떻게 받는가? |
즉,
블로킹/논블로킹 → "제어권"의 관점
동기/비동기 → "결과 처리"의 관점입니다.
🧭 5. 4가지 조합으로 이해하기
| 구분 | 설명 | 예시 |
| ① 블로킹 + 동기 | 요청이 끝날 때까지 코드 멈춤 | fs.readFileSync() |
| ② 논블로킹 + 동기 | 즉시 반환하되, 직접 결과를 반복 확인(polling) | 오래된 select() 방식 |
| ③ 블로킹 + 비동기 | 백그라운드에서 비동기 수행하지만, 호출 쪽은 join 대기 | 일부 Thread API |
| ④ 논블로킹 + 비동기 | 요청 즉시 반환 + 콜백/Promise로 결과 처리 | ✅ Node.js I/O 모델 |
🧠 6. 시각화로 정리
시간 흐름 →
───────────────────────────────────────────────
[Blocking Sync]
|--- I/O 요청 --- (대기) --- 완료 후 결과 반환 ---|
[Non-blocking Sync]
|--- I/O 요청 (즉시 반환) --- 결과확인? --- 결과확인? ---|
[Blocking Async]
|--- async 요청 --- (대기) --- 완료 ---|
[Non-blocking Async]
|--- 요청 (즉시 반환) --- (다른 일 수행) --- 콜백/이벤트로 결과 수신 ---|
Node.js가 사용하는 모델은 논블로킹 + 비동기 (Non-blocking + Async) 입니다.
이 조합이 가장 효율적이며, 고성능 서버의 핵심 구조입니다.
🧩 7. 핵심 요약
| 기준 | 블로킹 / 논블로킹 | 동기 / 비동기 |
| 초점 | 호출 직후 제어권 반환 여부 | 결과를 언제/어떻게 받는가 |
| 관점 | 함수 실행 시점 | 결과 처리 시점 |
| 대표 예시 | readFileSync() vs readFile() | 직접 반환 vs 콜백/Promise |
| Node.js 모델 | ✅ 논블로킹 + 비동기 |
🚀 8. 마무리
Node.js의 핵심은 단순히 “비동기 함수”가 아니라,
논블로킹 I/O 모델과 이벤트 루프 구조를 결합한 데 있습니다.“입출력은 OS가 처리하고, JS는 기다리지 않는다.”
이것이 바로 Node.js가 싱글 스레드임에도
고성능 서버를 구현할 수 있는 이유입니다.
🧾 작성 참고
이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.