[cookbook] module.exports === exports -> true, 하지만, call by reference를 지키지 않으면 안된다.

Introduction

node.js 에서 실행되는 파일 혹은 require되는 module은 모두 익명함수에 wrap되어 실행됩니다. 이때, 해당 모듈안에는 5가지의 로컬변수가 전달됩니다. 그것은 아래와 같습니다.

  1. exports - module.exports 의 참조
  2. require - require 함수를 포함한 객체
  3. module - module.exports 로서 사용되고 있음, 실제 require 함수로부터 리턴되는 겍체입니다.
  4. __filename - 파일명
  5. __dirname - 디렉토리명

이것중에 exports와 module.exports는 실행되는 파일이나 require 함수에서 사용되고 있습니다. 이것의 사용상 혼돈을 줄이기 위해 실제 내막을 파헤치고 입문자님들이 혼돈을 겪지 않도록 가이드라인을 작성해드립니다.

require Native source 엿보기

require 소스는 node.cc에서 v8::Script::Compile 되기 전에 javascript 로서 존재합니다. 아래 소스는 require 함수의 일부분입니다. 소스의 주석을 확인하세요

function NativeModule(id) {
   ...
   this.exports = {}; // ★  by reference 전달됩니다. 이것만 기억하시면 모두 해결됩니다.
   ...
}

NativeModule.require = function(id) {
   ...
   var nativeModule = new NativeModule(id);
   nativeModule.compile();
   ...
   // 실제 리턴되는 놈은 module.exports 입니다. 가장 중요한 부분입니다. 꼭 기억하세요.
   // exports 가 by reference규칙을 아무리 잘 지켜도
   // 리턴되는 module.exports가 실세입니다. 이놈은 by reference규칙을 깨버려도 무조건 리턴됩니다.
   return nativeModule.exports;
};

NativeModule.wrap = function(script) {
   return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};

// 모든 모듈 혹은 node a.js 에서 a.js 조차도 모두 wrapping 되어 실행됩니다.
// 확인방법 console.log(arguments);
NativeModule.wrapper = [
   '(function (exports, require, module, __filename, __dirname) { ',
     '\\ n);'
];

NativeModule.prototype.compile = function() {
   var source = NativeModule.getSource(this.id);
   source = NativeModule.wrap(source);

   var fn = runInThisContext(source, this.filename, true);

   // ★ this.exports 전달하는것과, this 부분 필수 기억하세요.
   // 즉, 같은 객체를 가리키고 있고, call by reference 적용됩니다
   fn(this.exports, NativeModule.require, this, this.filename);

   this.loaded = true;
};

추천관련글

module.exports 와 exports 는 차이점이 없다.

ECMAScript는 변수를 해석할 때, primitive value인지 reference value인지 판단해야 합니다. call by reference가 성립될 수 있는 변수의 타입은 reference values (Object)이고, call by value가 성립될 수 있는 변수의 타입은 primitive type (undefined, null, boolean, number, string) 입니다.

위 Native 소스를 보시면 module.exports와 exports변수는 call by reference 관계가 형성됩니다. module 만들때 exports 로 하나 module.exports 로 하나 동일합니다. 단, 돌려주는 값은 module.exports입니다. 사용상 부주의가 일어나는 부분은 객체의 프로퍼티만 변형해야 하는데, 이를 따르지 않는 행위때문에 module.exports와 exports사이에 지키기로 한 call by reference 약속이 깨지면서 발생합니다. 이점을 꼭 기억하세요.

그럼 mymodule.js 파일안에서 module.exports와 exports를 혼용하는 여러경우를 비교해보겠습니다.

실습 #1

// 각각 객체의 프로퍼티만 수정합니다.
exports.foo = "hello";
module.exports.bar = "node";

$ node -e "console.log(require('./test'))"
{ foo: 'hello', bar: 'node' }

exports와 module.exports는 call by reference 를 잘 지키고 있습니다. 모두 살아남았습니다.

실습 #2

exports.foo = "hello";
// module.exports를 새로운 객체로서 선언합니다.
// exports.foo는 낙동강 오리알 신세
module.exports = {bar: "node"};

$ node -e "console.log(require('./test'))"
{ bar: 'node' }

module.exports는 call by reference를 Crack하고 있습니다. exports와는 이별하는군요. 결국, 리턴되는 module.exports 만 살아남았습니다.

실습 #3

// 이번엔 exports가 혼자 살라고 새로운 객체를 선언합니다.
// exports는 실제로 리턴되는 놈이 아니기 때문에, 프로퍼티만 접근하여 call by reference를 유지해야합니다. 즉, 이런 행위는 아무런 의미가 없다는 거죠. 
exports = {foo: "hello"};
module.exports.bar = "node";

$ node -e "console.log(require('./test'))"
{ bar: 'node' }

exports가 혼자 살아 볼려다가 즉사했습니다.

실습 #4

// 이번에도 혼자 살라고 새로운 객체를 선언합니다. 무의미하다고 했는데 말이죠
exports = {foo: "hello"};
// 이에 질세라 module.exports도 새로운 객체를 선언합니다. 누가 이길까요?
module.exports = {bar: "node"};

$ node -e "console.log(require('./test'))"
{ bar: 'node' }

역시 exports 는 죽었군요. ㅋㅋ, 역시 리턴되는 놈이 짱입니다.

실습 #5

// 이번에는 reference로서 전달된 exports = {} 이것을 또 혼자 바꿀려고 합니다.
// exports가 살길은 프로퍼티만 변형해야하는데 말이죠.
exports = {foo: "hello"};

$ node -e "console.log(require('./test'))"
{}

역시 exports 는 module.exports를 이겨볼라다가 죽었군요. module.export에 기생해야합니다:)

실습 #6

// 오호. 이제 기생할려고 제대로 설정하는군요.
exports.foo = "hello";

$ node -e "console.log(require('./test'))"
{ foo: "hello" }

이것이 바로 exports가 홀로 사용될 시 사용방법입니다. by reference 전달된 규칙을 깨지만 않으면 됩니다.

마지막 실습

module.exports = "hello";
exports.foo = function() {
    console.log('hello node');
};

이번엔 exports 미안했는지 call by reference 를 지키기 위해 프로퍼티에만 접근하여 함수를 선언했습니다. 그러나 실세인 module.exports가 call by reference를 깨버리고 primitive type인 string형식으로 새롭게 선언해버립니다. exports와 module.exports의 지켜진 약속은 깨졌네요.

# node -e "var o = require('./test'); console.log(o); o.foo();"
hello
undefined:1
^
TypeError: Object hello has no method 'foo'
   at Object.<anonymous> (eval at <anonymous> (eval:1:82))
   at Object.<anonymous> (eval:1:70)
   at Module._compile (module.js:444:26)
   at startup (node.js:80:27)
   at node.js:551:3

당근 exports는 아무리 바락을 해도 module.exports를 넘을 수가 없군요.

결론

기억하세요. this.exports = {} 으로 선언되어 있고, by reference로서 전달됩니다. 그리고 require의 리턴값은 exports가 아닌 module.exports 입니다. module.exports와 exports를 같이 사용하는 경우를 줄이고, 같이 사용한다면 call by reference 를 꼭 지켜 둘다 살아남으시기 바랍니다.

KIN 플~

추천 관련글