Terminal에서 node index.js를 치면 무슨일이 일어날까?

index.js 파일안에 내가 쓴 javascript코드는 잘 실행되는데, 조금 더 큰 그림을 보고 싶었다. 그래서 nodejs 소스코드를 다운받아서 중요한것 같은 부분만 읽보고 실행 순서를 추측해 보았다.

심플하게 정리

  1. node index.js → main()함수 실행
  2. Global Module( console.log , timer 등등의 기본 모듈) 로드
  3. 내 index.js 로드 및 실행
  4. event-loop 실행

실제 코드

아래는 꼬리에 꼬리를 물듯이 node.js 안에서의 함수 호출과 정의를 따라 가본 과정이다. 언어는 C++과 Javascript이다. 함수의 내용물중 내가 잘 모르거나, 필요없어보이는 것들은 삭제하였다.

// 시작은 main 함수!

// src/node_main.cc : 94
int main(int argc, char* argv[]) {
  return node::Start(argc, argv);
}


// src/node.cc : 3036
int Start(int argc, char** argv) {
    
  // 아래는 무엇을 하는지 잘 모르겠지만, V8 을 쓰기위해서 준비하는것 같다
  PlatformInit();
  Init(&args, &exec_args);
  InitializeV8Platform(per_process_opts->v8_thread_pool_size);
  V8::Initialize();
    
  // 들어가는 인자값이 다른 Start()함수가 3개정도 되는것 같다
  const int exit_code = Start(uv_default_loop(), args, exec_args);
}


// src.node.cc : 2988
inline int Start(uv_loop_t* event_loop, const std::vector<std::string>& args, const std::vector<std::string>& exec_args) {
  
  // isolate는 javascript 코드를 실행하는 Virtual Machine의 독립된 복사본이라고 합니다
  Isolate* const isolate = NewIsolate(allocator.get(), event_loop);
    
  // 또 Start() 함수.
  exit_code = Start(isolate, isolate_data.get(), args, exec_args);
}


// src.node.cc : 2881
inline int Start(Isolate* isolate, IsolateData* isolate_data, const std::vector<std::string>& args, const std::vector<std::string>& exec_args) {{
    
  Environment env(isolate_data, context, v8_platform.GetTracingAgentWriter());
  env.Start(args, exec_args, v8_is_profiling); // process object 생성
  
  // 이 부분이 중요하다. 
  // lib/internal/bootstrap/loaders.js & lib/internal/bootstrap/node.js을 메모리에 로드한뒤 실행시킨다.
  // 나의 index.js코드도 여기서 실행된다.
  // 아래의 긴~코드들은 여기를 시작점으로 한다.
  LoadEnvironment(&env);
  
  // 여기서 이제 루프를 돌린다.
  uv_run(env.event_loop(), UV_RUN_DEFAULT);
}

// src/node.cc : 2116
void LoadEnvironment(Environment* env) {
  Local<String> loaders_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/loaders.js");
  MaybeLocal<Function> loaders_bootstrapper =
      GetBootstrapper(env, LoadersBootstrapperSource(env), loaders_name); // loaders.js 코드를 실행한다
  Local<String> node_name =
      FIXED_ONE_BYTE_STRING(env->isolate(), "internal/bootstrap/node.js");
  MaybeLocal<Function> node_bootstrapper =
      GetBootstrapper(env, NodeBootstrapperSource(env), node_name); // node.js 코드를 실행한다
}

// src/node.cc : 2068
static MaybeLocal<Function> GetBootstrapper( Environment* env, Local<String> source, Local<String> script_name) {
  // javascript 코드(string)를 실행한다. 가상머신이라는게, javascript 코드를 실행하면서 메모리를 관리해주는 역할을 하는것 같다.
  MaybeLocal<Value> bootstrapper_v = ExecuteString(env, source, script_name);
}


// src/node.cc : 1008
static MaybeLocal<Value> ExecuteString(Environment* env, Local<String> source, Local<String> filename) {
  // 컴파일 하고
  MaybeLocal<Script> script = Script::Compile(env->context(), source, &origin);
  
   // 실행 !
  MaybeLocal<Value> result = script.ToLocalChecked()->Run(env->context());
}

loader.js와 node.js를 로드해서 실행하는데, 해당 파일들 안에서 일어나는 일들을 확인해 보자

// loader.js를 실행하고 나서, node.js를 실행하는데, node.js안에서는 이렇게 함수가 호출되어있다. 
// 즉, loader.js에서 load에 필요한 함수들을 정의한 뒤에, node.js에서 그걸 사용하면서 실제로 load를 진행한다.
startup();

// lib/internal/bootstrap/node.js : 31
function startup() {
  // Make process.argv[1] into a full path.
  const path = NativeModule.require('path');
  process.argv[1] = path.resolve(process.argv[1]);

  // loader.js를 먼저 실행했기 때문에 node.js에서 가져와 쓸 수 있다
  const CJSModule = NativeModule.require('internal/modules/cjs/loader.js/loader');

  preloadModules();

  const fs = NativeModule.require('fs');
  const filename = CJSModule._resolveFilename(process.argv[1]);
  const source = fs.readFileSync(filename, 'utf-8'); // 나의 index.js파일을 읽어들인다
  checkScriptSyntax(source, filename); // 문법 체크
    
  CJSModule.runMain();
}
  

// lib/internal/modules/cjs/loader.js : 733
Module.runMain = function() {
  // argv[1] 안에 index.js가 들어있다.(아마도...)
  Module._load(process.argv[1], null, true);
  
  // Handle any nextTicks added in the first tick of the program
  process._tickCallback();
};
    
// lib/internal/modules/cjs/loader.js : 506
Module._load = function(request, parent, isMain) {

  // Don't call updateChildren(), Module constructor already does.
  var module = new Module(filename, parent);

  tryModuleLoad(module, filename);

  return module.exports;
};
    

// lib/internal/modules/cjs/loader.js : 539
function tryModuleLoad(module, filename) {
  module.load(filename);
}


// lib/internal/modules/cjs/loader.js : 593
Module.prototype.load = function(filename) {
  Module._extensions[extension](this, filename); // == Module._extensions['.js'](this, filename)
};
    
    
// lib/internal/modules/cjs/loader.js : 702
Module._extensions['.js'] = function(module, filename) {
  var content = fs.readFileSync(filename, 'utf8'); // 파일을 HardDisk에서 읽어 들여서
  module._compile(stripBOM(content), filename); // 컴파일!
};
    

// lib/internal/modules/cjs/loader.js : 654
Module.prototype._compile = function(content, filename) {

  // create wrapper function
  var wrapper = Module.wrap(content);

  var compiledWrapper = vm.runInThisContext(wrapper, {
    filename: filename,
    lineOffset: 0,
    displayErrors: true
  });
};
    
// lib/internal/modules/cjs/loader.js : 125
Module.wrap = function(script) {
  return Module.wrapper[0] + script + Module.wrapper[1];
};

// lib/internal/modules/cjs/loader.js : 129
Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];
    
// wrapper가 실행되면 이렇게 된다.
(function (exports, require, module, __filename, __dirname) { 
  // 내 index.js code가 여기에 들어간다
  // 이런 연유로 exports()나 require()을 내 코드에서 쓸 수 있다
});

// lib/vm.js : 287
function runInThisContext(code, options) {
  return createScript(code, options).runInThisContext(options);
}
    
// lib/vm.js : 89
// Script의 public 함수
runInThisContext(options) {
  return super.runInThisContext(...args);
}
    
    
// lib/vm.js : 40
// ContextifyScript가 super다
class Script extends ContextifyScript {  ~~  }

// lib/vm.js : 25
const {
  ContextifyScript,
  makeContext,
  isContext: _isContext,
  compileFunction: _compileFunction
} = internalBinding('contextify');
    
// src/node_contextify.cc
// 이렇게 등록했기 때문에 아까, javascript 단에서 super.runInThisContext()를 쓸 수 있게된것이다.( c++ 함수를 javascript에서도 쓸수있도록 연결 )
env->SetProtoMethod(script_tmpl, "runInThisContext", RunInThisContext);
    
// src/node_contextify.cc
static void RunInThisContext(const FunctionCallbackInfo<Value>& args) {
  Environment* env = Environment::GetCurrent(args);

  // Javascript가상머신이 javascript코드를 실행한다
  EvalMachine(env, timeout, display_errors, break_on_sigint, args);
}
    
// src/node_contextify.cc
static bool EvalMachine(Environment* env,
                          const int64_t timeout,
                          const bool display_errors,
                          const bool break_on_sigint,
                          const FunctionCallbackInfo<Value>& args) {

  Local<Script> script = unbound_script->BindToCurrentContext();
  result = script->Run(env->context());

}

// 여기까지가 LoadEnvirenment !
// 이 모든 과정이 끝난 다음에, node.cc로 다시 돌아간다. 그리고 event-loop를 돌린다.

참고 자료

  1. learning-nodejs
  2. nodejs source code

마지막으로

지적과 질문은 환영입니다. 읽어주셔서 감사합니다.

Leave a Reply

Your email address will not be published.