개요

Node.js가 스크립트를 실행할 때 어떤 구성요소가 어떤 순서로 초기화되고 동작하는지 정리 바이너리 기동부터 모듈 로딩, V8 파싱과 실행, 이벤트 루프와 비동기 작업 처리까지의 전체 흐름을 개발자 관점에서 간결하게 설명

핵심 개념

  • V8 엔진, Ignition 바이트코드와 JIT 최적화
  • libuv, 비동기 I O 백엔드와 이벤트 루프 단계
  • 모듈 시스템, CommonJS와 ES Module의 로딩 차이
  • 전역 실행 컨텍스트와 런타임 내장 객체
  • 마이크로태스크 큐와 process.nextTick의 우선순위

실행 순서 요약

  1. Node 바이너리 시작
  2. 런타임 초기화와 내부 바인딩 준비
  3. 모듈 로더 기동 및 엔트리 파일 로드
  4. V8 파싱과 바이트코드 컴파일
  5. 전역 실행 컨텍스트 구성과 최상위 코드 실행
  6. 비동기 작업 등록
  7. 이벤트 루프 진입
  8. 비동기 콜백 처리 반복
graph TD
A[Node 시작] --> B[V8, libuv 초기화]
B --> C[모듈 로딩]
C --> D[파싱 및 컴파일]
D --> E[최상위 코드 실행]
E --> F[이벤트 루프]
F --> G[비동기 처리 반복]

단계별 동작

1단계 Node 바이너리 시작

node yourfile.js 실행으로 C++ 엔트리 포인트가 기동됨 V8, libuv, 내부 바인딩 계층이 초기화되고 런타임 전역 상태가 준비됨

2단계 런타임 초기화

V8, libuv, 암호화와 압축 등 필수 의존성 초기화 process 객체와 argv, env, versions 설정 표준 입출력 스트림 바인딩 준비

3단계 모듈 로더 실행

엔트리 파일을 기준으로 모듈 해석과 로딩 수행

  • CommonJS require 사용 시 동기 로딩과 함수 래핑 적용
    • (function exports, require, module, **filename, **dirname { … }) 형태로 감쌈
    • **filename, **dirname 은 CommonJS에서만 제공됨
  • ES Module import 사용 시 비동기 로딩과 링크 단계 수행
    • mjs 확장자 또는 package 설정의 type module 조건에서 활성화됨
    • ESM에는 **filename, **dirname 미제공이며 import.meta.url 사용 권장 의존성 그래프를 구축하고 모듈 결과를 캐시에 보관함

4단계 파싱과 바이트코드 컴파일

V8이 소스 코드를 AST로 파싱한 뒤 Ignition 바이트코드로 컴파일함 실행 중 반복 패턴이 관찰되면 JIT 최적화가 적용됨

5단계 전역 실행 컨텍스트 구성

ExecutionContext를 만들고 전역 객체와 내장 바인딩을 연결함 global, process, require CommonJS 한정, import ESM 문맥 등 환경이 확정됨 엔트리 파일의 최상위 레벨 코드가 동기적으로 실행됨

6단계 비동기 작업 등록

setTimeout, Promise, 파일 I O, 네트워크 I O 등은 즉시 실행 대신 콜백을 적절한 큐에 등록함

  • 타이머 큐, I O 큐, 체크 큐 등 이벤트 루프 단계별 큐에 배치됨
  • 마이크로태스크는 별도 큐에 등록됨 이 시점까지는 콜백이 실행되지 않고 등록만 진행됨

7단계 이벤트 루프 진입

초기 스크립트 평가가 끝나면 이벤트 루프로 진입함 대표 단계 순서

  1. timers 단계에서 setTimeout, setInterval 콜백 처리
  2. pending callbacks 단계에서 일부 시스템 콜백 처리
  3. idle, prepare 단계 내부 용도
  4. poll 단계에서 I O 처리 및 대기
  5. check 단계에서 setImmediate 처리
  6. close callbacks 단계에서 close 이벤트 처리 각 콜백 실행 직후에 process.nextTick 큐와 마이크로태스크 큐가 소진됨
  • 우선순위는 process.nextTick 먼저, 그 다음 Promise then, queueMicrotask 순으로 처리됨

8단계 비동기 콜백 처리 반복

대기 중인 큐에서 콜백을 꺼내 실행하고 필요 시 새로운 비동기 작업을 다시 등록함 작업이 남아있는 동안 루프가 계속 순환함

CJS와 ESM 로딩 차이 요약

  • require 동기 로딩과 캐시 기반 재사용, 파일 해석과 실행이 호출 시점에 일어남
  • import 비동기 로딩과 링크, 정적 의존성 해석 우선, 토폴로지 정리 후 실행
  • 런타임 전역 제공 요소 차이 존재 CommonJS의 **filename, **dirname은 ESM에 없음

마이크로태스크와 nextTick 주의

  • process.nextTick은 Node 전용 큐로 매 단계와 콜백 직후 최우선 처리됨
  • Promise then 등 마이크로태스크는 nextTick 처리 이후 실행됨
  • 과도한 nextTick 사용은 이벤트 루프 굶김을 유발할 수 있음

종료 조건

  • 활성 타이머, 보류된 I O, 등록된 콜백 등 이벤트 루프에서 처리할 작업이 더 이상 없음
  • 명시적 process.exit 호출

간단 예시

아래 코드는 동기 실행과 마이크로태스크, 타이머의 상대적 순서를 보여줌

console.log('start')

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

Promise.resolve().then(() => {
  console.log('promise')
})

console.log('end')

실행 순서 1 start 동기 2 end 동기 3 promise 마이크로태스크 4 timeout 타이머

정리

Node.js는 바이너리 기동 후 V8과 libuv를 초기화하고 모듈을 로딩한 뒤 최상위 코드를 실행함 비동기 작업은 등록만 수행되고 초기 평가가 끝나면 이벤트 루프에 진입하여 단계별로 콜백을 처리함 process.nextTick이 마이크로태스크보다 먼저 실행되는 점, CommonJS와 ESM의 로딩 및 전역 차이를 구분할 것

참고자료