본문 바로가기

Backend/Node.js

모듈 시스템

Node.js 로 작업을 하다보면 굉장히 많은 모듈을 만들게 되는데요. 어플리케이션을 구성하기 위한 작은 조각에서부터 사내에서 공통으로 사용하기 위한 공통 모듈까지 명시적으로 export 해서 많이 사용합니다. 혹은 그와 반대로 export 하지 않은 것들을 비공개로 유지해

정보 은닉화에 사용되기도 하죠.

 

- Revealing module pattern

 

노출식 모듈 패턴이라고도 불리는 이 기법은 상당히 보편적인 방식입니다. 아래 예시 코드를 보도록 하겠습니다.

 

const testModule = (() => {
  const privateTest1 = () => {
    console.info('private test1');
  };

  const exported = {
    test1: () => {
      console.info('test1');
    },
    test2: () => {
      console.info('test2');
    },
  };

  return exported;
})();

console.info(testModule); // { test1: [Function:test1], test2: [Function:test2] }

 

저는 이런 형태로 클로저를 자주 사용하기도 하는데, 위 코드에선 private 범위를 만들고 공개될 부분만 export 합니다.

따라서 외부에선 export 된 test1, test2 API 만 사용이 가능하고, testModule 내부의 privateTest1 과 같은 내부 컨텐츠엔 접근할 수 없는 형태입니다.

 

- CommonJS

 

CommonJS 는 Javascript 용 모듈 생태계에 대한 공통의 규칙을 수립하는 것을 목표로 하는 프로젝트입니다. (관련 링크)

아마 가장 많이 사용되는 방식일거라 생각되고, Node.js 는 이 스펙 위에 모듈 시스템을 구축했습니다.

CommonJS 의 어떤 배경이나 역사에 대해서 이 포스팅에선 더 깊게 다루진 않겠습니다.

 

모듈에서 중요한 것은 세가지입니다.

 

  • 스코프 : 모듈은 독립적인 실행 영역을 갖습니다.
  • 정의 : export 를 통해 노출할 모듈을 정의합니다.
  • 사용 : 노출된 모듈을 require 로 사용합니다.
// 외부로 노출
module.exports = {
  run: () => { ... },
};

// 사용
const { run } = require('...');

 

 

Node.js 사용자라면 너무나 익숙할 문법이며, 우리는 모듈을 정의하고 사용할 때 이러한 형태를 기본으로 잡고 있습니다.

require 함수는 동기적으로 동작합니다. 초창기에는 Node.js 가 비동기 버전의 require 를 사용했었다고 합니다만, 복잡성이 과도해 곧

제거되었다고 합니다. 약간은 다른 방향이긴 하지만 근래엔 dynamic import 와 같은 것들도 생기긴 했죠.

 

require 의 호출은 어느정도 캐싱이 적용되지만, 그로 인해 순환 의존성이라는 문제를 갖고 있기도 합니다.

 

// x.js
exports.loaded = false;

const y = require('./y');

module.exports = {
  wasLoaded: y.loaded,
  loaded: true,
};


// y.js
exports.loaded = false;

const x = require('./x');

module.exports = {
  wasLoaded: x.loaded,
  loaded: true,
};


// main.js
const x = require('./x');
const y = require('./y');

console.info(x); // { wasLoaded: true, loaded: true }
console.info(y); // { wasLoaded: false, loaded: true }

 

위 코드 중 main.js 에서 x, y 의 require 순서를 바꾸면 x, y 의 출력 또한 바뀝니다.

두 모듈 모두 require 로 불려지면 완전히 초기화되지만, y.js 에서 x.js 모듈을 로드할 때 로드가 완료되지 않습니다.

이로 인해 불완전하게 셋팅되는 문제가 발생하고, 어떤 모듈이 먼저 로딩되는지에 따라 결과가 바뀌는 문제는 꽤 큰 버그로 이어질 수도 있습니다. 가끔은 프로젝트에서 사용하는 외부 모듈에서도 이러한 문제가 발견되고, 패치 버전을 적용하는 등의 이슈가 있었던 것 같습니다.

 

- 모듈 정의

 

모듈을 정의하는 방식은 exports 를 사용하기도하고, module.exports 를 사용하가도 합니다. 예시로 보겠습니다.

 

// logger.js
exports.info = message => {
  console.log('info:', message);
};

exports.error = message => {
  console.log('error:', message);
};

// main.js
const logger = require('./logger');

logger.info('info message'); // info: info message
logger.error('error message'); // error: error message

 

exports 로 모듈을 정의하는 건 가장 기본적인 방법입니다. 비슷하게  module.exports 를 보도록 하죠.

 

// logger.js
module.exports = message => {
  console.log('info:', message);
};

module.exports.error = message => {
  console.log('error:', message);
};

// main.js
const logger = require('./logger');

logger('info message'); // info: info message
logger.error('error message'); // error: error message

 

위의 exports 와 비교해보면, 모듈의 진입점에 대한 단일 기능을 제공해 사용을 단순화하면서도 최소한의 노출을 할 수 있다는 점입니다.

하지만 error 를 추가로 붙인것처럼 모듈을 확장하는 것도 가능합니다.

 

이러한 예시들이 있긴 하지만, Node.js 에선 하나의 모듈이 하나의 기능만 노출하는 것을 권장합니다.

단일 책임 원칙을 지킬 것을 강력히 권하며, 책임은 모듈에 의해 완전히 캡슐화 되어야 합니다.

 

위의 exports 와 module.exports 에 대해 짧은 설명을 덧붙이자면, exports 가 module.exports 를 call by reference 로 바라보고 있습니다. 따라서 리턴되는 값은 항상 module.exports 이며, exports 에 공개할 멤버를 추가하는 것은 결국 module.exports 에도 같은 멤버가 추가되는 것입니다. exports -> module.exports -> {} 와 같은 형태이며, 이 점만 알아두면 모듈을 사용할 때 이 두가지가 혼용되어 있어도 잘못 해석하는 일은 없을 것입니다.

'Backend > Node.js' 카테고리의 다른 글

Promise & Promisify  (0) 2022.08.20
Observer Pattern in Node.js  (0) 2022.08.13
Callback 패턴  (0) 2022.07.23
lockfile 주의점  (0) 2022.07.16
Node.JS - Reactor 패턴  (0) 2022.07.09