Node.js에서 스크립트가 실행될 때 어떤 일들이 일어날까요?

혹시 Node.js 스크립트를 실행하면 어떤 방식으로 동작을 하는지 궁금하지 않았나요?

이 글은 이런 궁금증을 가지고 있는 분들에게 조금이나마 도움이 되고자 하는 글이니, 관심이 없으시다면 스킵하셔도 됩니다 :D

먼저, Node.js를 분석하기 위해서 코드를 다운 받아봅시다.

$ git clone https://github.com/nodejs/node.git && cd node

Node.js의 파일 구조는 아래와 같은 모습을 가지고 있습니다.

$ tree -L 1
.
├── AUTHORS
├── BSDmakefile
├── BUILDING.md
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── COLLABORATOR_GUIDE.md
├── CONTRIBUTING.md
├── CPP_STYLE_GUIDE.md
├── GOVERNANCE.md
├── LICENSE
├── Makefile
├── README.md
├── SECURITY.md
├── android-configure
├── benchmark
├── common.gypi
├── configure
├── configure.py
├── deps
├── doc
├── lib           # JS sources
├── node.gyp
├── node.gypi
├── src           # C++ sources
├── test
├── tools
└── vcbuild.bat

7 directories, 20 files

노드는 크게 2종류의 언어로 이루어져 있는데 lib/ 폴더 안에 있는 JavaScript 파일 들과 src/ 폴더 밑에 존재하는 많은 양의 C++ 파일 들이 있습니다.

Node.js의 모든 시작 과정은 C++에서 완료되며, main 시작 지점은 src/node_main.cc 입니다.

이 안에서 제일 중요한 부분은 다음과 같습니다.

return node::Start(argc, argv);

이 것은 src/node.ccStart을 호출하고 command line의 parameter들을 argcargv로 넘겨줍니다.

첫 번째 단계로 Start()안에서 런타임 초기화 작업들이 수행되는데 InitializeOncePerProcess는 환경변수(예.NODE_OPTIONS)나 CLI parameters(예.--abort-on-uncaught-exception)를 통해 주어지는 설정 값들을 처리하고 V8을 초기화 해준다. 한 번 이작업이 완료가 되면, 새로운 node 인스턴스들은 libuv default loop를 사용해 초기화 된 후 마침내 아래와 같이 실행되게 됩니다.

NodeMainInstance main_instance(&params,
                               uv_default_loop(),
                               per_process::v8_platform.Platform(),
                               result.args,
                               result.exec_args,
                               indexes);
result.exit_code = main_instance.Run()

NodeMainInstance::Run()을 통해 Node를 실행 시 어떤 일들이 수행되는 지를 알 수 있는 길에 좀 더 가까워졌습니다. 새로운 메인 쓰레드 수행 환경이 src/node_main_instance.cc에서 만들어지는데:

std::unique_ptr<Environment> env = CreateMainEnvironment(&exit_code);

Environment 인스턴스가 libuv와 V8을 접근 할 수 있는 Handle들을 가지고 있는 노드 프로세스의 중심 객체입니다.

이 객체를 LoadEnvironment에 넘기면

LoadEnvironment(env.get());

main thread 수행이 시작된다:

void LoadEnvironment(Environment* env) {
  CHECK(env->is_main_thread());
  USE(StartMainThreadExecution(env));
}

이 부분에서 C++에서 JavaScript 수행으로 변경이 됩니다.

MaybeLocal<Value> StartMainThreadExecution(Environment* env) {
  if (NativeModuleEnv::Exists("_third_party_main")) {
    return StartExecution(env, "internal/main/run_third_party_main");
  }

  std::string first_argv;
  if (env->argv().size() > 1) {
    first_argv = env->argv()[1];
  }

  if (first_argv == "inspect" || first_argv == "debug") {
    return StartExecution(env, "internal/main/inspect");
  }

  if (per_process::cli_options->print_help) {
    return StartExecution(env, "internal/main/print_help");
  }


  if (env->options()->prof_process) {
    return StartExecution(env, "internal/main/prof_process");
  }

  // -e/--eval without -i/--interactive
  if (env->options()->has_eval_string && !env->options()->force_repl) {
    return StartExecution(env, "internal/main/eval_string");
  }

  if (env->options()->syntax_check_only) {
    return StartExecution(env, "internal/main/check_syntax");
  }

  if (!first_argv.empty() && first_argv != "-") {
    return StartExecution(env, "internal/main/run_main_module");
  }

  if (env->options()->force_repl || uv_guess_handle(STDIN_FILENO) == UV_TTY) {
    return StartExecution(env, "internal/main/repl");
  }

  return StartExecution(env, "internal/main/eval_stdin");
}

StartExecution의 경우 두번째 인자로 넘겨지는 JS파일들을 읽고, 컴파일하고, 실행하는데 모든 파일은 lib/ 폴더 밑에 있는데, 이중 관심있게 봐야하는 곳은 두 곳 입니다.

if (!first_argv.empty() && first_argv != "-") {
  return StartExecution(env, "internal/main/run_main_module");
}
if (env->options()->force_repl || uv_guess_handle(STDIN_FILENO) == UV_TTY) {
  return StartExecution(env, "internal/main/repl");
}

lib/internal/main/repl.jslib/internal/main/run_main_module.js 두 스크립트 모두 중요한 시작 메소드인 lib/internal/bootstrap/pre_execution.js안의 prepareMainThreadExecution을 실행하는데, 이것은 여러 setup 작업들을 수행하는데 이 중 CommonJS와 ES module loader 초기화 역시 수행됩니다.

lib/internal/modules/cjs/loader.js안의 Module 객체는 CommonJS loaders core이며 initializeCJSLoaderrunMain를 통해 lib/internal/modules/run_main.js안의 executeUserEntryPoint을 실행해줍니다.

CommonJS module의 경우 Module._load로 새로운 Module 인스턴스를 생성하고 load를 호출 해주고 적절한 extension function이 module을 읽어오는데 사용됩니다.

Module._extensions[extension](this, filename);

*.js extension이 실제 파일을 읽고 컴파일 해줍니다.

const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename)

module._compile은 V8의 ScriptCompiler:CompileFunctionInContext를 호출하고 node module wrapper에 맞는 exports, require, module, __filename, __dirname을 넘겨줍니다.

이렇게 불려진 function은 실행이 되고 결과를 리턴해줍니다:

result = compiledWrapper.call(thisValue, exports, require, module,
                              filename, dirname);

다음으로 확인 해봐야할 부분은 libuv eventloop입니다.

JavaScript를 컴파일하고 실행한 후, node 인스턴스는 event loop를 시작해줍니다.

do {
    uv_run(env->event_loop(), UV_RUN_DEFAULT);

    per_process::v8_platform.DrainVMTasks(isolate_);

    more = uv_loop_alive(env->event_loop());
    if (more && !env->is_stopping()) continue;

    if (!uv_loop_alive(env->event_loop())) {
        EmitBeforeExit(env.get());
    }

    // Emit `beforeExit` if the loop became alive either after emitting
    // event, or after running some callbacks.
    more = uv_loop_alive(env->event_loop());
} while (more == true && !env->is_stopping());

uvrunmode UV_RUN_DEFAULT는 더이상 동작하거나 참조되는 곳이 없을때 까지 event loop를 수행합니다.

libuv는 lifetime에 따라 handle과 request로 구분이 되는데, 오랫동안 수행되는 객체의 경우 handle이라고 할 수 있고 짧은 시간동안 수행되는 operation들을 request라고 할 수 있습니다.

const http = require('http');

const requestHandler = (req, res) => {
  res.write('Hello World!');
  res.end();
};

const server = http.createServer(requestHandler);

server.listen(8080);

위 예제의 경우 requestHandler를 libuv request로 볼 수 있습니다. 반면, server 객체의 listen은 handle이라고 할 수 있습니다.

HTTP server를 멈추지 않는 이상, libuv는 계속해서 실행되고 입력되는 연결들을 처리할 것 입니다.


위 과정들이 Node.js에서 스크립트가 어떻게 수행되는 지를 설명한 내용입니다. 물론, 전체 과정을 상세히 다루는게 아니기 때문에 설명이 부족한 부분이 있을 수 있겠지만, 스크립트가 수행되는 과정에서의 중요한 부분들을 설명해주고 있습니다.

혹시 기회가 된다면 다음 포스트를 통해 추가적인 내용들을 다루어 보겠습니다.


Reference

Inside node: What happens when we execute a script?

© 2020 HunseopJeong, Built with Gatsby