개요
Node.js가 스크립트를 실행할 때 어떤 구성요소가 어떤 순서로 초기화되고 동작하는지 정리 바이너리 기동부터 모듈 로딩, V8 파싱과 실행, 이벤트 루프와 비동기 작업 처리까지의 전체 흐름을 개발자 관점에서 간결하게 설명
핵심 개념
- V8 엔진, Ignition 바이트코드와 JIT 최적화
- libuv, 비동기 I O 백엔드와 이벤트 루프 단계
- 모듈 시스템, CommonJS와 ES Module의 로딩 차이
- 전역 실행 컨텍스트와 런타임 내장 객체
- 마이크로태스크 큐와 process.nextTick의 우선순위
실행 순서 요약
- Node 바이너리 시작
- 런타임 초기화와 내부 바인딩 준비
- 모듈 로더 기동 및 엔트리 파일 로드
- V8 파싱과 바이트코드 컴파일
- 전역 실행 컨텍스트 구성과 최상위 코드 실행
- 비동기 작업 등록
- 이벤트 루프 진입
- 비동기 콜백 처리 반복
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단계 이벤트 루프 진입
초기 스크립트 평가가 끝나면 이벤트 루프로 진입함 대표 단계 순서
- timers 단계에서 setTimeout, setInterval 콜백 처리
- pending callbacks 단계에서 일부 시스템 콜백 처리
- idle, prepare 단계 내부 용도
- poll 단계에서 I O 처리 및 대기
- check 단계에서 setImmediate 처리
- 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의 로딩 및 전역 차이를 구분할 것