Node.js 이벤트 루프와 논블로킹 I/O로 만드는 고성능 서버

Node.js는 V8 엔진과 libuv 기반 논블로킹 I/O로 싱글 스레드에서도 수천~수만 연결을 처리하는 서버사이드 JavaScript 런타임이다. 이벤트 루프 구조와 실무 패턴, 코드 예시를 정리한다.

2026-08-12 · 최초 발행 2025-11-26

브라우저의 자바스크립트가 서버로 넘어온 이유

브라우저 RIA 시대의 JavaScript 활용을 서버로 확장한 런타임이 Node.js다. Google의 V8 엔진 위에서 이벤트 루프와 Non-Blocking I/O를 결합해 고성능 네트워크 서버를 구현하는 플랫폼이다. 싱글 스레드 기반의 이벤트 루프 모델과 libuv 스레드 풀을 통한 비동기 I/O 처리 방식이 실무에서 가치를 갖는다.

정의하면 Node.js는 V8(JavaScript Engine) 위에서 동작하는 서버사이드 JavaScript 런타임이다. 핵심 메커니즘은 싱글 스레드 이벤트 루프 + libuv 기반 Non-Blocking I/O + 스레드 풀 오프로딩이다. 고성능 네트워크 서버·실시간 애플리케이션을 구현하고, 단순한 동시성 모델과 광범위한 내장 네트워킹 스택을 제공하는 게 목적이다.

하나의 스레드가 수만 개 연결을 처리하는 구조

Node.js RuntimeClientV8 EngineTCP/HTTPI/O Calldispatch non-blockingI/O completecallbacks/microtasksgenerateCPU-bound taskerror eventhandle/rejectuncaughtRequestResponseUser JS Code(async/await, Promise,callback)Node Bindings / N-API(C/C++ Addons)Node Standard Library(http, fs, net, http2)Event Loop (libuv)Thread Pool (libuv)OS KernelSockets, Files, DNSWorker Threads/ClusterProcess Exit / DomainHandler

절차를 보면 입력은 소켓 수신 → 이벤트 루프 큐 진입이다. 처리는 JS 콜백/프로미스 실행 → I/O 요청은 libuv를 통해 스레드 풀로 오프로딩 → 완료 이벤트 재큐잉 → 콜백 재실행 순서다. 출력은 응답 직렬화 후 소켓 전송이다. 에러는 콜백의 첫 인자나 프로미스 reject로 전파되고, 미처리 예외는 process.on('uncaughtException')이나 종료 처리로 다뤄야 한다. 싱글 스레드 JS 실행이 보장되므로 I/O 경합은 커널·라이브러리 수준에서 처리된다.

단일 스레드에서 JS를 실행해 컨텍스트 스위칭 오버헤드를 최소화하고, 동시성은 이벤트 루프 큐와 비동기 콜백/프로미스 체인으로 구성한다. 파일·네트워크·DNS I/O는 논블로킹 호출로 위임하고 완료 시 콜백을 재개하며, CPU 바운드나 블로킹이 필요한 작업은 worker_threads/Cluster로 격리한다.

V8과 네이티브 확장, 내장 네트워킹 스택

V8의 JIT 최적화는 빠른 JS 실행을 보장한다. Node-API(N-API)와 C/C++ Addon으로 네이티브 확장을 구현하면 JS-네이티브 경계 비용을 최소화할 수 있다. http/https/http2, net, tls, fs 같은 표준 라이브러리를 내장 제공하므로 추가 의존성 없이 서버를 구현할 수 있다. 다만 최신 프로토콜·보안 옵션의 지원 범위는 버전별로 다르니 최신 정보를 확인해야 한다.

플랫폼 독립성은 libuv가 Windows/macOS/Linux 이식성을 확보하는 방식으로 달성된다. 라이선스는 Node.js MIT, V8 BSD로 단순한 체계를 갖는다.

어디에 쓰이는가

실시간 서비스는 채팅·게임·콜라보레이션 앱의 WebSocket 이벤트를 처리한다. 고성능 API 게이트웨이/BFF는 다수 백엔드로의 팬아웃 호출을 비동기로 병렬화한다. 스트리밍/프록시는 스트림 파이프라인으로 대용량 미디어·로그를 중계한다. 서버리스 함수는 콜드스타트와 경량 런타임을 활용한 이벤트 드리븐 처리에 쓰인다. IoT/에지 환경은 리소스 제한 속에서 소켓 중심의 이벤트 처리에 적합하다.

실무 패턴으로는 I/O 바운드를 우선 배치하고 CPU 바운드 로직은 워커 스레드나 전용 서비스로 분리한다. stream.pipeline을 사용한 백프레셔 적용으로 고비용 다운스트림을 보호하고, 회로 차단기·재시도·타임아웃을 표준화해 네트워크 안정성을 강화한다.

Node.js와 전통적 스레드-풀 서버의 차이

지표 Node.js(논블로킹 이벤트 루프) 전통적 스레드-풀 서버(블로킹)
성능 I/O 바운드 고효율, 낮은 오버헤드 CPU 바운드 유리, 스레드 전환 비용 존재
확장성 수만 연결 핸들링, 가벼운 스케일아웃 스레드 수/메모리 한계 영향
일관성 싱글 스레드 JS 실행 일관성 요청별 스레드 컨텍스트 분산
안정성 이벤트 루프 스타베이션 주의 필요 스레드 격리로 한 요청 실패 영향 제한
운영 편의 배포/메모리 풋프린트 소형 JVM/애플리케이션 서버 튜닝 복잡성

설계 원칙과 하지 말아야 할 것들

설계 목표는 네 가지로 정리된다. JS 코드에서 동기 I/O를 회피하고 콜백/프로미스 기반 비동기를 쓰는 Direct I/O 금지 지향, HTTP/HTTPS/HTTP2 등 내장 스택 중심의 Built-in 네트워킹, libuv로 OS 차이를 추상화하는 플랫폼 독립성, Node.js MIT·V8 BSD의 라이선스 단순화다.

모범사례는 서버 경로에서 fs.readFileSync 등 동기 API 사용을 금지하고 부트스트랩/CLI에서만 제한적으로 허용하는 것, async 함수 최상위 try/catch와 전역 process handlers를 최소화하는 에러 처리 표준화, 소켓 타임아웃·최대 헤더 크기·커넥션 풀 제한 같은 자원 관리, TLS 기본값 강화·Helmet/CSP 적용·의존성 취약점 스캔 자동화 같은 보안이다.

트레이드오프도 명확하다. CPU 바운드 작업에는 불리해 워커 스레드나 외부 서비스로 오프로딩해야 하고, 장기 실행 동기 루프는 이벤트 루프 스타베이션을 유발하므로 타이머나 분할 실행으로 해소해야 한다.

코드로 보는 비동기 I/O와 팬아웃

사전 조건은 Node.js LTS 권장(예: v20+)이며, 정확한 LTS 버전은 최신 정보를 확인해야 한다. Linux/macOS/Windows에서 공통으로 동작한다.

비동기 파일 I/O + HTTP 서버:

node -v
# v20.x 이상 권장
// server.js
// 목적: 동기 I/O 금지, 비동기 스트리밍과 백프레셔 적용
import http from "node:http";
import { createReadStream } from "node:fs";
import { pipeline } from "node:stream";
import { promisify } from "node:util";

const pump = promisify(pipeline);

const server = http.createServer(async (req, res) => {
  res.setHeader("Content-Type", "text/plain; charset=utf-8");
  if (req.url === "/large") {
    try {
      await pump(createReadStream("./large.txt"), res); // 스트리밍 + 백프레셔
    } catch (err) {
      res.statusCode = 500;
      res.end("stream error");
    }
    return;
  }
  res.end("ok");
});

server.listen(3000, () => console.log("http://localhost:3000"));

외부 API 팬아웃 병렬화:

// fanout.js
// 목적: I/O 대기 숨김, 타임아웃/에러 병합 처리
import http from "node:http";

const fetchWithTimeout = (url, ms = 1500) =>
  Promise.race([
    fetch(url),
    new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms)),
  ]);

http
  .createServer(async (_, res) => {
    try {
      const [a, b] = await Promise.all([
        fetchWithTimeout("https://api.example.com/a"),
        fetchWithTimeout("https://api.example.com/b"),
      ]);
      const [ja, jb] = await Promise.all([a.json(), b.json()]);
      res.setHeader("Content-Type", "application/json");
      res.end(JSON.stringify({ a: ja, b: jb }));
    } catch (e) {
      res.statusCode = 502;
      res.end("upstream error");
    }
  })
  .listen(3001);

운영 체크리스트는 PM2/systemd로 프로세스를 관리하고 무중단 재시작을 설정하는 것, max_old_space_size·ulimit·소켓/파일 핸들 한도를 조정하는 것, 헬스체크/레디니스 엔드포인트를 제공하고 OpenTelemetry·로그·메트릭 같은 관측성을 내장하는 것이다. CPU 바운드 작업 분리, 백프레셔·타임아웃·에러 표준화 같은 모범사례를 적용하면 안정적인 고성능 서버를 구현할 수 있고, 신규 API 게이트웨이·실시간 서비스·스트리밍 중계 같은 영역에서 우선 도입할 만하다.

Node.js이벤트 루프논블로킹 I/OV8libuv