간단히 요약하면..
구성 요소
V8은 ECMAScript 명세를 구현한 JavaScript 실행 엔진으로, 자체적으로 힙(heap), 스택(stack), 실행 컨텍스트(Execution Context) 구조를 가진다. 즉, V8만으로도 JavaScript 언어 자체 기능(변수, 함수, 클래스, 프로미스 등) 은 실행할 수 있다.
Node.js는 V8을 임베드(embed) 하면서 파일 시스템, 타이머, 네트워크 요청 등 V8에는 없는 기능을 C++로 구현한 API를 JS 환경에 노출한다. 또한 이러한 API들은 비동기 처리를 위해 libuv를 사용하며, libuv는 OS의 비동기 I/O 를 다룬다.
주요 기능
- Node.js가 초기화될 때, C++ 계층에서 V8에 JS 함수 이름과 C++ 함수 포인터를 바인딩한다. 예를 들어 fs.readFile() JS 함수는 실제로 C++의 FSReqWrap::ReadFile()을 가리킨다.
- JS 코드에서 함수를 호출하면, Node는 이 객체를 감싸는(wrap) C++ 객체(*)(예: TCPWrap) 를 생성하여 이 내부에 libuv가 제공하는 구조체(uv_req_t 또는 uv_handle_t) 포함시킨다(이 구조체의 내부 data 필드에 다시 Wrap을 할당하여 서로 참조 가능하다). 이 객체(*)는 JS 콜백에 대한 포인터도 가지고 있어, libuv가 I/O 완료 시 C++ 계층에서 V8에 해당 콜백을 실행할 수있도록 요청을 보낸다.
흐름 제어
V8 바인딩을 설정하는 등의 초기화 이후, 노드의 메인 스레드는 이벤트루프 로직을 반복한다(libuv의 uv_run (...) { while(..) {...} })
uv_run은 while문을 반복하며 poll. check, timer 등의 phase를 실행하고, 각 phase는 해당하는 큐가 비어있지 않는동안 그 안의 로직을 반복한다.
종류
노드 인스턴스의 종류를 메인 인스턴스와 워커 인스턴스(스레드풀과는 다르다)로 나눌 수 있는데, 각각의 V8 isolate와 이벤트루프를 갖는다.
코드로 살펴보기!
node app.js 와 같은 명령어를 통해 Node 로 JS 파일을 실행했을 때 Node.js 내부에서는 어떤 것들이 실행되는지, main함수부터 따라가 보았다. SEA(단독으로 실행가능한 애플리케이션) 모드가 아닌 표준 모드로, unix 기준으로 살펴보았다.

1. V8 바인딩 설정
main()
└─ node::Start()
└─ StartInternal()
└─ InitializeOncePerProcessInternal()
└─ InitializeNodeWithArgsInternal()
└─ binding::RegisterBuiltinBindings()
└─ ...
main 함수에서 시작해서 위와 같은 과정을 통해 ResiterBuiltinBindings 함수가 실행되는데, 이 함수는 X-Macro 패턴을 사용하여 그 다음의 코드를 자동생성한다.

void RegisterBuiltinBindings() {
_register_fs();
_register_tcp_wrap();
_register_timer_wrap();
// ... 기타
}
_register_XXX 함수들은 node.js 소스코드가 컴파일 되는 시점에 NODE_BINDING_CONTEXT_AWARE_INTERNAL 매크로가 호출한 NODE_BINDING_CONTEXT_AWARE_CPP 매크로에 의해 생성되어 있는 상태이다. 예를 들어, src에 tcp_wrap.cc가 포함된 상태로 node.js를 컴파일하면 TCP::Initialize 함수의 구현부로 _register_tcp_wrap 함수가 생성되고, 노드가 초기화될 때 이 함수가 실행되게 된다.


TCPWrap::Initialize함수는 JS에서 호출 가능한 TCP 관련 메서드들을 Node.js의 C++ 구현과 연결한다.
이를 위해 V8의 FunctionTemplate을 사용하여 JS 함수의 설계도(블루프린트)를 만들고, 각 메서드 이름에 대응하는 C++ 콜백 함수를 등록한다. 이렇게 등록된 함수는 JS 코드에서 해당 메서드가 호출될 때 V8이 내부적으로 C++ 콜백을 실행하도록 동작한다.


아래의 TCPWrap::Listen 함수는 TCPWrap 객체를 만든다음 libuv의 uv_listen() 을 호출해서 uv_tcp_t 핸들을 OS 이벤트루프에 “리스닝 소켓”으로 등록하고, 새 연결이 들어오면 OnConnection 콜백을 호출하도록 설정한 다음, 그 결과 코드를 JS로 돌려준다.

2. uv loop 설정 및 생성, V8 Isolate 생성
main()
└─ node::Start()
└─ StartInternal()
└─ ... (1번)
└─ NodeMainInstance()


3. Environment (노드의 메인 객체) 생성
main()
└─ node::Start()
└─ StartInternal()
└─ ... (1번)
└─ NodeMainInstance()
└─ ... (2번)
└─ NodeMainInstance::Run()
└─ CreateMainEnvironment()
└─ CreateEnvironment()
4. JS 엔트리 파일을 실행
main()
└─ node::Start()
└─ StartInternal()
└─ ... (1번)
└─ NodeMainInstance()
└─ ... (2번)
└─ NodeMainInstance::Run()
└─ CreateMainEnvironment()
└─ ... (3번)
└─ LoadEnvironment()
└─ StartExecution()
└─ Realm::ExecuteBootstrapper("internal/main/run_main_module")
└─ BuiltinLoader::CompileAndCall()
└─ Function::Call()
└─ [lib/internal/main/run_main_module.js 실행]
└─ Module.runMain(mainEntry)
└─ executeUserEntryPoint()
main 함수에서 시작해서 위와 같은 과정을 통해 executeUserEntryPoint 함수가 실행된다. 이 때 아래 코드가 실행된다고 가정해보자.
// app.js
const net = require('net');
const server = net.createServer((socket) => {
console.log('client connected');
socket.write('hello from server!\n');
socket.end();
});
server.listen(8080, () => {
console.log('server listening on port 8080');
});
server.listen(...)이 실행되면 net.js의 해당 부분을 통해

JS: server.listen()
└─ Server.prototype.listen()
└─ listenInCluster()
└─ server._listen2() (= setupListenHandle)
└─ createServerHandle()
└─ this._handle.bind(address, port)
└─ this._handle.listen(backlog)
이러한 함수들을 지나서. 위에서 나온 TCPWrap::Listen 함수가 실행되게 된다.
5. libuv 이벤트루프로 들어간다.
다음으로 아래의 함수를 따라 libuv의 uv_run이 node.js 프로세스에서 실행되어, I/O 작업과 그 콜백 작업의 실행 흐름을 제어한다.
main()
└─ node::Start()
└─ StartInternal()
└─ NodeMainInstance::Run()
└─ ... (3번)
└─ NodeMainInstance::Run(ExitCode*, Environment*)
└─ SpinEventLoopInternal()
└─ uv_run()
이 내용은 별도의 포스팅으로 다루겠다.
Node.js 딥다이브 (2) - libuv 의 I/O 멀티플렉싱 살펴보기
서론ibuv는 Node.js와 독립적인 비동기 I/O 추상화 계층이지만, Node.js의 이벤트 루프와 비동기 작업 흐름을 담당하므로 함께 정리하였다.I/O 멀티플렉싱이란..I/O 작업은 유저 스페이스에서 할 수 없
9436188.tistory.com
메모
V8 핸들
TCPWrap.cc 코드에 Local<XXX> 타입이 있다. 이것은 V8 핸들이다.
JS 객체(obj)는 V8 이 관리하는 힙에 존재하며 GC에 의해 이동하거나 해제할 수 있기 때문에 직접 포인터로 잡아두면 위험하다. 대신 V8이 “핸들 테이블(handle table)”에 객체를 등록해주고 그 인덱스를 통해 안전하게 접근하도록 하는 목적이다.
빌트인 스크립트
네이티브 빌트인 스크립트는 노드에 내장된 파일로, 노드 사용 시 접근할 수는 없으며 미리 컴파일 되어있어 내장로더로 실행시 초기화 성능을 최적화할 수 있다.
가장 처음에 app.js를 찾고 읽는 것을 C++레벨에서 직접 구현할 줄 알았는데, 이 스크립트를 실행하도록 하여 일관성과 유연성 측면에서 강점을 가진다고 한다.
'기술 조사 > Node.js' 카테고리의 다른 글
| AsyncHooks 를 이용하여 비동기작업과 콜백을 연결하기 (0) | 2025.11.12 |
|---|---|
| Node.js C++ Codebase 영한 번역 (0) | 2025.11.09 |
| inspect 옵션으로 CPU 프로파일 얻고 분석하기 (0) | 2025.10.09 |
| Node.js 딥다이브 (2) - libuv 의 I/O 멀티플렉싱 살펴보기 (3) | 2025.05.03 |
| Node.js에서 멀티코어를 활용하기 (8) | 2025.01.01 |