Node.js 이벤트 루프 아키텍처 — 단일 스레드 런타임이 대규모 동시성을 처리하는 방법

발행: (2026년 5월 25일 AM 12:28 GMT+9)
8 분 소요
원문: Dev.to

출처: Dev.to

Node.js를 처음 다루기 시작했을 때, 한 가지가 마음에 들지 않았습니다.
“단일 스레드 시스템이 어떻게 동시에 수천 개의 요청을 처리할 수 있지?”
모순된 것처럼 보였지만, 공식 문서를 통해 이벤트 루프를 제대로 이해하고 나니 모든 것이 맞아떨어졌습니다.
이 글은 깊이를 잃지 않으면서 가능한 한 가장 간단하게 설명하려는 시도입니다.

Node.js는 흔히 “단일 스레드”라고 불리지만, 그 말은 불완전합니다.

  • JavaScript 실행은 하나의 메인 스레드에서 이루어집니다.
  • 하지만 Node.js 자체는 한 번에 하나의 작업만 수행한다는 제한이 없습니다.

Node.js는 다음을 활용합니다.

  • OS 커널
  • 백그라운드 스레드(libuv)
  • 비동기 I/O

→ 따라서 올바른 표현은 다음과 같습니다.

Node.js는 JavaScript 실행은 단일 스레드이지만, I/O 처리는 다중 시스템으로 동작한다.

이 구분이 핵심입니다.

Node.js는 작업이 끝날 때까지 기다리지 않습니다.

다음과 같은 흐름을 따릅니다.

  1. 요청을 받는다.
  2. 작업(DB 호출, 파일 읽기, API 호출 등)을 시작한다.
  3. 기다리지 않는다.
  4. 다음 요청으로 넘어간다.
  5. 결과가 준비되면 나중에 다시 처리한다.

이를 논블로킹 I/O라고 합니다.

→ 공식 문서 인용:

상상해 보세요:
당신은 웨이터(= 이벤트 루프)이고, 주방은 OS/백그라운드 워커입니다.

  • 주문을 받고 → 주방에 전달 → 완성된 요리를 서빙한다.
  • 직접 요리를 만들거나, 한 주문이 끝날 때까지 대기하지 않는다.

이것이 바로 Node.js가 확장성을 갖는 방식입니다.

이벤트 루프는 단순한 큐가 아니라 단계(phase) 로 구성됩니다.

  • TimerssetTimeout(), setInterval() 콜백 실행
  • Pending Callbacks – 시스템 레벨 콜백 처리(TCP 오류 등)
  • Idle / Prepare – 내부용, 직접 다루지는 않음
  • Poll Phase (가장 중요) – 새로운 I/O 이벤트 수집, I/O 콜백 실행, 할 일이 없으면 대기
  • Check PhasesetImmediate() 콜백 실행
  • Close Callbacks – 정리 콜백 실행(socket.on('close') 등)
   ┌───────────────────────────┐
   │           timers          │
   └─────────────┬─────────────┘

                 v
   ┌───────────────────────────┐
┌─>│     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │      close callbacks      │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤           timers          │
   └───────────────────────────┘

→ 루프는 위 단계들을 계속 순환합니다.
여기서 마법이 일어나며,

  • 들어오는 요청은 여기서 처리되고,
  • 완료된 비동기 작업은 여기로 돌아옵니다.

아무 작업도 대기 중이지 않다면 Node는 효율적으로 대기합니다.

→ 그래서 Node.js는 CPU 사이클을 낭비하지 않고 높은 확장성을 유지합니다.
이 점은 대부분 간과되는 중요한 디테일입니다.

process.nextTick() vs setImmediate()

  • process.nextTick()

    • 현재 함수가 끝난 직후 바로 실행
    • 이벤트 루프가 계속 진행되기 전에 실행 → 남용하면 I/O를 차단할 수 있음
  • setImmediate()

    • 다음 반복(iteration)의 check phase에 실행
    • 더 안전하고 예측 가능

→ 공식 문서 권장: 실제 상황에서는 setImmediate()를 우선 사용.

이제 핵심 질문을 살펴봅시다.

  • 각 요청마다 새로운 스레드가 생성 → 메모리 부담, 컨텍스트 스위칭 오버헤드
  • 단일 스레드가 모든 요청을 처리 → 요청당 스레드 생성 없음, 비동기 콜백 사용

예를 들어 1,000명의 사용자가 서버에 접근하면 Node.js는:

  1. 모든 요청을 등록하고 비동기 작업을 시작한다.
  2. 이벤트 루프는 자유롭게 유지된다.
  3. 응답이 돌아오면 콜백을 큐에 넣고, 이벤트 루프가 차례대로 실행한다.

결과: 낮은 자원 사용량으로 높은 동시성 달성.

Node.js가 빛을 발하는 경우

  • DB 쿼리
  • 외부 API 호출
  • 파일 시스템 작업
  • 스트리밍
  • 실시간 애플리케이션(채팅, 소켓)

하지만 적합하지 않은 경우

  • 무거운 CPU 연산
  • 큰 동기 루프

왜냐하면 이벤트 루프를 블로킹하면 전체 서버가 멈추기 때문입니다.

while (true) {}

→ 서버 전체가 정지합니다.

process.nextTick()을 남용하면 이벤트 루프가 굶주림 상태가 되어 I/O 실행이 차단됩니다.

fs.readFileSync()와 같은 동기식 API는 프로덕션에서는 피해야 합니다.

Node.js는 프로세스당 단일 스레드이지만, 다음 방법으로 확장할 수 있습니다.

  • Cluster 모듈
  • Worker Threads
  • 로드 밸런서

→ 이를 통해 멀티코어 활용수평 확장이 가능합니다.

정리

공식 Node.js 문서를 읽고 실제로 앱을 만들어 보면, 저는 이렇게 생각합니다.

  • Node.js는 한 번에 모든 일을 하려는 것이 아니라 절대 블로킹하지 않으려 한다.
  • 이벤트 루프는 똑똑한 코디네이터다:
    • 준비된 작업을 실행하고,
    • 기다리는 작업은 건너뛰며,
    • 시스템을 계속 움직이게 한다.

→ 그래서 Node.js는 수천 개의 동시 요청을 처리할 수 있다 – 병렬 실행 덕분이 아니라 효율적인 스케줄링과 논블로킹 설계 덕분이다.

한 줄로 요약하자면:

Node.js는 작업을 빠르게 수행하기 때문이 아니라, 불필요하게 기다리지 않는 것이 뛰어나기 때문에 확장한다.

이 사고방식이 잡히면 Node.js 아키텍처의 모든 것이 이해가 됩니다.

0 조회
Back to Blog

관련 글

더 보기 »

내 스킬

프로젝트를 위한 AI 지시문을 만들고, 설치하고, 관리하세요 — 코딩이 필요 없습니다. CREATE 이름을 정하고, 카테고리를 선택하고, 원하는 것을 설명하세요 — 마법사가 자동으로 구성합니다.