Skip to main content

Command Palette

Search for a command to run...

Node.js의 논블로킹 I/O 완전 정복 — 이벤트 루프부터 비동기 동작까지

Published
•4 min read•View as Markdown

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("이 코드는 파일 읽기 후 실행됨");

동작 흐름

  1. 파일을 읽기 시작

  2. 파일 읽기 완료될 때까지 콜 스택이 멈춤

  3. 완료 후 data를 반환

  4. 다음 줄 실행


⚡ 논블로킹 I/O 예시

const fs = require("fs");

fs.readFile("file.txt", "utf-8", (err, data) => {
  console.log("파일 내용:", data);
});
console.log("이 코드는 먼저 실행됨");

동작 흐름

  1. readFile() 호출 → 즉시 반환

  2. 파일 읽기 요청은 libuv의 I/O 스레드 풀에서 처리

  3. 읽기 완료 시 → 이벤트 루프가 콜백 큐에 등록

  4. 콜 스택이 비면 콜백 실행

출력 결과:

이 코드는 먼저 실행됨
파일 내용: (file.txt 내용)

즉, I/O 자체는 백그라운드에서 비동기적으로 수행되며,
JS 스레드는 계속해서 다음 코드를 실행할 수 있습니다.


🔄 3. 이벤트 루프(Event Loop)와 논블로킹의 연결

Node.js의 이벤트 루프는 다음과 같은 사이클로 동작합니다.

  1. 콜 스택(Call Stack) 이 비면,

  2. 큐(Queue) 안에 있는 마이크로태스크(Promise) → 매크로태스크(setTimeout 등) 순서로 콜백을 꺼냄

  3. 각 콜백을 실행하면서 새로운 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의 도움을 받아 내용을 정리하였습니다.

More from this blog

LangGraph의 astream_events: 에이전트 실행을 실시간으로 들여다보기

💡 한 줄 요약: astream_events()는 LangGraph 그래프 실행 중 발생하는 모든 이벤트(LLM 토큰, 툴 호출, 노드 시작/종료 등)를 실시간 스트림으로 받을 수 있는 비동기 API입니다. 들어가며 AI 에이전트가 응답을 생성하는 데 25초가 걸린다고 상상해보세요. 사용자는 평균 8초 후에 페이지를 이탈합니다. 하지만 스트리밍을 도입

Jun 20, 20266 min read

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

Dev note

32 posts