2장 (2/3): Node.js의 모듈 시스템

이 글은 Node.js의 모듈 시스템에 대해 소개한다.


1. 모듈 시스템의 필요성과 Javascript의 방식

모듈 시스템은 프로그램의 구성 요소들 간의 역할을 분리하고, 의존 관계와 구현 상세를 격리하는데 필수적이다. 모듈 시스템의 문법으로 보면, 소스 파일간의 import, export를 하는 것인데, 개념 상 Java의 접근 제한자 - private, protected, public - 도 모듈의 역할 중 일부를 수행 한다고 할 수 있다.

Javascript 모듈 시스템으로는 대표적으로 ESM, CommonJs 라는 두 개의 기술이 있는데, 현재의 Node.js는 ESM, CommonJs를 모두 지원한다.

종류 ESM CommonJS
제정 시기 ES6에 제정됨 ESM 이전의 대표적인 비표준
문법(Node 기준) import / export require / module.export
Node.js 지원 여부 Yes Yes
Browser 지원 여부 최신 브라우저에서 지원 CommonJs.js 로딩 필요

자세한 역사와 기타 모듈 시스템의 종류는 JavaScript 표준을 위한 움직임: CommonJS와 AMD | Naver D2를 참고.


2. Revealing Module Pattern

Javascript에는 접근 제한자가 없다. 접근을 원천적으로 제한하는 방법 중, 공개할 부분만 객체로 담아 내보내는 패턴이 있다. Private 변수는 클로저를 통해 접근할 수 있으므로, 꽤 괜찮은 방법이다.

Revealing Module 패턴을 구현하는 방법은 대표적으로 IIFE(즉시 실행 함수 표현식)가 있다. IIFE는 익명 함수를 ()로 감싼 후 즉시 실행하는 함수 호출 방식이다.

1
2
3
4
5
6
7
8
9
10
11
12
const module = (() => {
const privateFoo = () => {
/* private functionality */
};
let privateCounter = 0;

const increase = () => ++privateCounter;
const decrease = () => --privateCounter;

// 이 객체를 반환하므로, 외부에선 privateFoo, Bar에 접근할 수 없다.
return { increase, decrease };
})(); // 즉시 실행하여, { increase, decrease } 객체가 반환된다.

3. CommonJs의 require 방식에 대해

CommonJs는 const moduleA = require('./moduleA');와 같이 모듈을 로딩하는 문법을 제공한다. require동기로 작동하고, 한 번 로딩한 모듈은 캐시된다. 내보낼 때에는 각 모듈별로 제공되는 exports 객체에 필드를 할당하는 방식으로 진행한다.

모듈은 캐싱되므로 항상 동일한 객체를 반환한다.

아래는 require의 수도 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const require = (modulePath) => {
// path를 가져오고, unique한 id로 활용한다.
const id = require.resolveAbsolutePath(
modulePath,
);

// 캐시된 모듈은 캐시를 반환한다.
if (require.cache[id])
return require.cache[id].exports;

// 처음 로딩하는 경우 새 exports 객체가 필요하다.
const module = {
exports: {},
id,
};

// 객체는 캐시한다.
require.cache[id] = module;

// 이 함수가 소스 코드를 읽어 exports 객체에 export 내용들을 할당한다.
readFileAndEvaluate(id, module, require);

return module.exports;
};

require.cache = {};
require.resolveAbsolutePath = (modulePath) => {
/* implementation */
};

어느 범위까지 같은 인스턴스가 반환될까?

  1. 같은 패키지로 빌드된다면 하나의 인스턴스를 공유할 것이다.

  2. package.json별로 독립적으로 dependency를 관리하기 때문에, 각 패키지간에 제 3의 모듈의 객체를 주고 받는 경우, 해당 객체는 버전 불일치가 있을 수 있다.

A Simple Explanation | Medium (EN)


4. 비동기 모듈 초기화

비동기로 객체를 초기화할 순 없다. require 함수가 동기로 작동하기 때문인데, 아무래도 initialize와 같은 메소드를 호출하는 형태로 비동기 API를 만들어서 활용하는 수 밖에 없을 듯하다.

관련 스택 오버 플로우 참고.


5. 순환 참조가 있는 경우

Node.js 환경에서 순환 참조를 하는 경우 한 모듈이 먼저 로딩되기 때문에, 동기로 로딩하는 경우, 한 쪽에서는 null, 한 쪽에서는 정상 로딩이 될 수 밖에 없다. 아니면 명확한 순서를 지정해준다면 해결할 수도 있겠지만(A[A.B = null]->B[B.A = A]->[A.B = B]), 순서를 명시하는 API가 따로 있는지 잘 모르겠다.

  1. 한 쪽에서 느린 초기화를 진행한다. (Lazy-Init) - 순서 정하기와 사실상 동일함.

  2. 순환 참조 관계에 있는 두 객체를 제 3의 객체에 의존하도록 한다. 관련 스택 오버 플로우 - 이 부분은 잘 이해하지 못 했다.


어떻게 export 해야 좋은 모듈일까?

1. Substack 패턴

모듈의 기능을 객체가 아닌 함수 단위로 노출한다. 진입점이자 주가 되는 함수를 module.exports로 내보내는데, 따라서 const logger = require('./logger')와 같이 바로 사용할 수 있는 함수가 된다. 또한, logger.verbose(msg); 와 같이 서브 함수들도 내보내, 사용하는 입장에서 기능의 중요도를 쉽게 파악할 수 있게 한다.

1
2
3
4
module.exports = mainFn;
exports.subFn1 = subFn1;
// 2...N-1
exports.subFnN = subFnN;

(ex)

1
2
3
4
5
6
7
// 메인 함수
module.exports = (msg) =>
console.log(`${this.name} ${msg}`);

// 서브 함수 1
exports.verbose = (msg) =>
console.log(`[verbose] ${this.name} ${msg}`);

2. 생성자 내보내기

prototype 기반으로 생성자를 만들거나, ES6 Class를 활용하여 생성자를 만들어, 생성자를 내보낸다. 사용하는 입장에선 객체의 기능을 확장할 수도 있고, 쉽게 인스턴스를 생성할 수도 있고, 사용하기도 깔끔한 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
module.exports = class Logger {
constructor(name) {
// implementation
}

log(msg) {
console.log(`${this.name} ${msg}`);
}

verbose(msg) {
console.log(`[verbose] ${this.name} ${msg}`);
}
};

3. 인스턴스 내보내기

생성자 내보내기와 거의 같지만, 싱글톤이 자동으로 구현되는 셈이므로 쉽게 활용하기 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Logger {
constructor(name) {
// implementation
}

log(msg) {
console.log(`${this.name} ${msg}`);
}

verbose(msg) {
console.log(`[verbose] ${this.name} ${msg}`);
}
}

module.exports = new Logger('App');

2장 (2/3): Node.js의 모듈 시스템

https://jsqna.com/ndp-2-module/

Author

Seongbin Kim

Posted on

21-01-01

Updated on

21-01-19

Licensed under

댓글