초보자를 위한 Node.js 이벤트 루프 설명

Backend2026-09-14TryQuickToolBox

Node.js가 단일 스레드이며 이벤트 루프를 사용해 수천 개의 동시 연결을 처리한다는 말을 들어보셨을 겁니다. 하지만 그게 실제로 무슨 의미일까요? Node.js가 처음이라면 이벤트 루프가 미스터리한 블랙박스처럼 느껴질 수 있습니다. 이 글에서는 더 나은, 더 빠른 Node.js 코드를 작성할 수 있도록 쉬운 언어로 설명합니다.

이벤트 루프가 존재하는 이유

전통적인 웹 서버(예: PHP를 사용하는 Apache)는 각 요청마다 새로운 스레드나 프로세스를 생성합니다. 이 방식은 작동하지만 동시 연결이 많을 때 확장성이 좋지 않습니다—각 스레드가 메모리와 CPU를 소비하기 때문입니다. Node.js는 다른 접근 방식을 취합니다: JavaScript를 단일 스레드에서 실행하고 이벤트 루프를 사용해 I/O 작업을 비동기적으로 처리합니다. 즉, 코드가 데이터베이스 쿼리나 파일 읽기가 끝날 때까지 기다리지 않고 다른 작업을 계속 실행하다가 결과가 준비되면 돌아옵니다.

핵심 구성 요소

이벤트 루프를 자세히 살펴보기 전에 주요 요소들을 명확히 합시다:

이벤트 루프의 작동 방식: 간단한 예제

다음 코드를 살펴보세요:

console.log('Start');

setTimeout(() => {
  console.log('Timeout');
}, 0);

console.log('End');

출력은 무엇일까요? Start, End, Timeout이라고 추측하셨다면 정답입니다. 타임아웃이 0밀리초임에도 콜백은 즉시 실행되지 않습니다. 무슨 일이 일어나는지 살펴보겠습니다:

  1. console.log('Start')가 콜 스택에 푸시되고, 실행되고, 팝됩니다.
  2. setTimeout이 호출됩니다. Node.js는 타이머를 등록하고 0ms 후에 만료되도록 설정합니다. 콜백이 저장되고 함수가 반환됩니다.
  3. console.log('End')가 푸시되고, 실행되고, 팝됩니다.
  4. 이제 콜 스택이 비어 있습니다. 이벤트 루프는 타이머 단계를 확인하고 타이머가 만료된 것을 확인합니다. 콜백을 콜 스택으로 이동시키고, Timeout을 로그에 출력합니다.

이것은 논블로킹 특성을 보여줍니다: 타이머가 나머지 코드를 차단하지 않습니다.

이벤트 루프의 단계

이벤트 루프는 여러 단계에서 콜백을 처리하며, 각 단계는 특정 목적을 가집니다. 이 단계들을 이해하면 실행 순서를 예측하는 데 도움이 됩니다.

단계 설명
Timers setTimeout()과 setInterval()로 예약된 콜백을 실행합니다.
Pending Callbacks 다음 루프 반복으로 지연된 I/O 콜백을 실행합니다.
Idle, Prepare Node.js 내부에서 사용됩니다.
Poll 새로운 I/O 이벤트를 가져오고, I/O 관련 콜백(타이머, setImmediate, close 콜백을 제외한 거의 모든 것)을 실행합니다.
Check setImmediate()로 예약된 콜백을 실행합니다.
Close Callbacks close 콜백을 실행합니다. 예: socket.on('close', ...).

각 단계 사이에 Node.js는 마이크로태스크를 확인합니다: process.nextTick() 콜백과 Promise입니다. 이들은 우선순위가 더 높으며 현재 작업이 완료된 직후, 다음 단계로 넘어가기 전에 실행됩니다.

마이크로태스크: nextTick과 Promise

마이크로태스크는 이벤트 루프의 단계에 속하지 않습니다; 각 단계 후와 각 콜백 후에 처리됩니다. 따라서 타이머와 I/O 콜백보다 먼저 실행됩니다. 예를 들어:

setTimeout(() => console.log('Timeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
process.nextTick(() => console.log('nextTick'));
console.log('Sync');

출력: Sync, nextTick, Promise, Timeout. 동기 코드가 먼저 실행되고, 그 다음 마이크로태스크(Promise보다 nextTick이 먼저), 그 다음 타이머가 실행됩니다.

경고: 재귀적인 process.nextTick() 호출은 이벤트 루프를 기아 상태로 만들 수 있어 I/O가 발생하지 못하게 할 수 있습니다. 재귀 작업에는 setImmediate()를 사용하세요.

setImmediate vs setTimeout

setImmediate()는 현재 poll 단계가 완료된 직후 콜백을 실행하도록 설계되었습니다. 반면, 0ms의 setTimeout()은 다음 타이머 단계를 기다립니다. 이들 사이의 순서는 컨텍스트에 따라 달라질 수 있지만, I/O 콜백 내부에서는 setImmediate()가 항상 먼저 실행됩니다.

이것이 코드에 중요한 이유

이벤트 루프를 이해하면 일반적인 함정을 피할 수 있습니다:

실용 예제: 논블로킹 파일 읽기

다음은 이벤트 루프가 논블로킹 I/O를 가능하게 하는 방법입니다:

const fs = require('fs');

console.log('Before read');

fs.readFile('large-file.txt', 'utf8', (err, data) => {
  if (err) throw err;
  console.log('File read complete');
});

console.log('After read');

출력: Before read, After read, 그 다음 File read complete. 파일 읽기는 백그라운드에서 발생하고, 완료되면 콜백이 큐에 들어갑니다. 그동안 다른 코드가 실행될 수 있습니다.

FAQ

Node.js는 정말로 단일 스레드인가요?

Node.js는 JavaScript를 단일 스레드에서 실행하지만, 파일 I/O, DNS 및 기타 작업을 위해 libuv 스레드 풀에서 여러 스레드를 사용합니다. 따라서 내부적으로 완전히 단일 스레드라고 할 수는 없습니다.

콜 스택과 이벤트 루프의 차이점은 무엇인가요?

콜 스택은 동기 코드를 실행합니다. 이벤트 루프는 비동기 콜백을 관리하며, 콜 스택이 비어 있을 때 큐에서 콜 스택으로 이동시킵니다.

여러 개의 이벤트 루프를 만들 수 있나요?

아니요, 각 Node.js 프로세스에는 하나의 이벤트 루프가 있습니다. 그러나 워커 스레드를 사용하면 각자 고유한 이벤트 루프를 가진 별도의 JavaScript 스레드를 실행할 수 있습니다.

성능 문제를 디버깅하기 위해 서버 로그를 다루고 있다면, Nginx Log Analyzer를 사용해 로그 데이터를 빠르게 파싱하고 시각화해보세요.