Nodejs top-level scope !== global

Run the code ‘console.log(this === global)' in your js file.

bbangjo
8 min readFeb 7, 2022

저번 글이 Javscript engine, 즉 v8에서 내부적으로 코드를 어떻게 처리하는지 살펴봤다면 이번엔 node our.js를 실행했을 때 내부적으로 our.js를 어떻게 해석하고 컴파일하고 실행하는지 살펴본다.

👀 Run console.log(this === global);

궁금증은 아주 간단한 js 코드로부터 시작한다. (node 인터프리터는 top-level scope가 global이므로 아래의 모든 코드는 node our.js로 실행해야 한다. )

흠 … 🤔 당연히 true가 나올 줄 알아서 좀 놀랬다. 이 결과의 이유는 Nodejs Docs에 설명되어 있다.

global

Added in: v0.1.27

<Object> The global namespace object.

In browsers, the top-level scope is the global scope. This means that within the browser var something will define a new global variable. In Node.js this is different. The top-level scope is not the global scope; var something inside a Node.js module will be local to that module.

nodejs 표준(=CommonJS의 명세)은 requiremodule.exports이다. 즉 node our.js를 실행하면, Nodejs는 이를 독립적인 실행 영역이 있는 module로 여기고 이를 v8로 컴파일 한 후 실행한다. 아래의 코드를 실행해보자.

?!?!?!?!?

신기하다… Nodejs는 참 신기하다…

이유는 위에서도 설명했 듯이 node our.js를 실행하면, Nodejs는 이를 독립적인 실행 영역이 있는 module로 여기고 이를 v8로 컴파일 한 후 실행하기 때문이다. 내부적으로 어떻게 our.js 코드를 module로 만드는지 살펴보자.

🔍 Internal

js에서 에러가 발생하면 콜 스택에 있는 함수가 모두 출력되기 때문에 디버깅할 때 용이하다. 일부러 에러를 발생시켜서 코드를 실행하는 단계를 따라가보자.

대략 살펴보면 run_main -> Module._load -> Module.load -> Object.Module._extensions..js -> Module._compile 의 과정을 거치는 것 같다. 코드는 모두 github에 공개되어 있으니 참고하면서 분석하면 될 것 같다.

분석에 사용한 버전은 16.13.2 이다.

$ node -v
v16.13.2

internal/main/run_main_module:

Module.runMain()을 실행한다. process.argv[1]node our.js 를 실행하기 때문에 ['node', 'our.js']에서 1번 인덱스에 있는 our.js이다. 값은 절대경로로 들어간다.

internal/modules/run_main:

여기서 useESMLoader 인지 확인하는데, 이는 아마 package.json에서 type:module이면 true가 되는 값인 것 같다. 뇌피셜임 ㅎㅎ 지금 우리의 경우에서는 false이므로 our.js를 인자로 주고 Module._load()를 실행하게 된다. 위의 에러코드에서도 당연히 확인할 수 있다.

internal/modules/cjs/loader:

run_main에서 이 함수를 호출할 때 다음과 같이 호출했다.

Module._load(main /*process.argv[1]*/, null, true);

const module = cachedModule || new Module(filename, parent); 에서 우리가 입력한 파일이름을 바탕으로 새로운 module 인스턴스를 생성한다. isMain은 true이기 때문에 process.mainModule에 새로 생성한 module 인스턴스를 넣는다. 이를 통해 global 스코프에 있는 process에서 module에 접근할 수 있다.

결과적으로 module.load(filename); 를 호출하게 된다.

internal/modules/cjs/loader:

filename에서 확장자를 뽑고, Module._extensions[extension](this, filename)를 호출한다. node our.js를 실행했기 때문에 extension은 js 이다.

internal/modules/cjs/loader:

filename을 통해 파일 내용을 읽어오고 content에 저장한다. 그리고 module._compile(content, filename);를 호출한다.

internal/modules/cjs/loader:

contentfilename을 통해 wrapSafe를 호출한다. 뭔가 이름이 우리가 찾고자 하는 답이 있을 것만 같은 이름이다.

wrapSafe:

코드 상에 Modulewrapper 프로퍼티를 정의하면서 patched 의 값이 true가 된다. Module.wrap을 호출하고 wrapping 된 함수를 vm에서 실행한 후 그 결과를 반환하는 식이다.

wrapper, wrap:

찾았다 !!!! 생각보다 허무하게 만들어주고 있었다. ㅋㅋㅋㅋ 그냥 파일 내용(코드)를 함수로 감싸줌으로써 module화 하고 있었다. wrapwrapper를 Module에 정의하는 부분은 생략 !

마지막으로 Module._compile 내부적으로 선언했던 exports, require, module, filename, dirname을 인자로 넘겨주며 함수를 실행하고 결과값을 리턴한다.

이제야 our.js를 실행했을 때 저런 결과가 나오는지 알게 되었다 !!!

🤔 require in global ??

좀 더 생각해볼만 한 게 있다. 평소 nodejs를 사용하다보면 require('fs'), require('express') 등등 require()을 정~~말 자주 사용한다. require() 함수를 지정해준 적이 없는데 어떻게 사용할 수 있을까? 대부분의 경우는 global 객체에 선언되어 있어서 사용할 수 있다 ! 가 정답이지만 require의 경우는 다르다. 아래 코드를 직접 실행해보자.

그럼 global에도 없는 함수를 어떻게 호출하고 사용할 수 있었던 걸까? 그 답이 위에 있다. Nodejs가 our.js를 실행하면 nodejs 표준(=CommonJS의 명세)에 따라 코드를 모듈화 하게 되고, 그 과정에서 우리의 코드는 다음과 같은 형태로 변하게 된다.

our code 코드에서 exports, require 등의 파라미터에 접근이 가능하다. 그리고 저 값들은 Module._compile에 선언되어 있다. 즉 우리가 평소 사용하는 require() 함수는 Module._compile에 정의되어 있는 require 함수 인 것이다 !!!!

진짜 신기하다 ㅋ!ㅋ!ㅋ!ㅋ!ㅋ!ㅋ!

그래서 우리는 require를 다음과 같은 방식으로도 접근할 수 있다.

Conclusion

브라우저와 nodejs 인터프리터의 경우에는 top-level scope가 global이다(브라우저는 window). 그리고 top-level scope에서 caller를 호출하면 null을 반환한다. 하지만 node our.js를 실행하는 경우는 nodejs 표준에 따라 코드를 모듈화하게 되고, 우리 코드는 그 모듈 안에서 실행되기 때문에 caller를 호출이 가능하다. 모듈의 exports, require, module, __filename, __dirname 파라미터는 모두 global 밖의 값이기 때문에 global에 있는 모든 값을 지우더라도 require는 호출이 가능하다 !!

긴 글 읽어주셔서 감사합니다 !!!

Reference

https://github.com/nodejs/node

https://nodejs.org/en/docs/

--

--