Node.js Module System

Aug 13, 2024

Server-Side Rendering 그리고 Node.js를 활용한 Server를 구축하기 시작하면서, 모듈 시스템에 대한 이해도가 부족한다는 점을 깨달았습니다.

특히, ECMAScript가 표준이 되기 이전부터 CommonJS가 널리 사용됨에 따라, 다양한 패키지를 설치하고 사용하는 과정에서 require 문과 import 문이 혼용되어 사용되는 일종의 문제를 정의하기 위해 정리하기 시작하였습니다.

Node.js 12부터 ECMAScript Modules 라는 새로운 모듈 시스템이 추가되고 이후 node 진영의 표준이 되면서, 두 모듈 시스템에 잘 대응하는 방법을 알아야 합니다.


아래의 글에서 CommonJSCJS, ECMAScript ModulesESM이라고 부르겠습니다.

1. What's the difference between CJS vs ESM?

기본적인 CJS vs ESM 의 차이는 다음과 같습니다.

1> CommonJS(CJS)

- 확장자
.js, .cjs
- 확장자 생략
가능
- Dynamic Import
가능
- index 생략
가능(e.g. require('./folder'))
- top level await
불가능
- _filename, _dirname, require, module.exports, exports
가능

2> ECMAScript(ESM)

- 확장자
.mjs
- 확장자 생략
불가능
- Dynamic Import
불가능
- index 생략
불가능
- top level await
가능
- _filename, _dirname, require, module.exports, exports
불가능, 대신, import.meta.url 사용

2. CommonJS(CJS)

CJS는 다음과 같은 특징이 중요합니다.

  • require / module.exports 를 사용합니다.
  • module loader가 synchronous 하게 작동합니다.
  • CJS에서 ESMimport 할 수 없습니다.
    • ESM에서 지원하는 top-level-await를 지원하지 않기 때문입니다.

module.exports, exports

// isOddOrEven.js
const odd = "홀수";
const even = "짝수";

module.exports = {
  odd,
  even,
};

// or

exports.odd = "홀수";
exports.even = "짝수";

// index.js
const odd = require("./isOddOrEven");

  • module.exports에 선언한 변수들을 담은 객체를 대입합니다.
  • exports 객체의 메서드로 키와 값을 명확하게 작성합니다.

→ 변수를 모아둔 Module로서 동작


Relationship between exports & module.exports

exports -> module.exports -> {}

exportsmodule.exports는 참조 관계에 있기 때문에, 한 모듈에 동시에 사용하지 않는 것이 좋습니다.


3. ECMAScript Modules(ESM)

  • 표준 공식 자바스크립트 모듈
  • Tree Shaking이 CJS보다 상대적으로 쉽게 가능합니다.
  • ESM에서는 CJS를 import 할 수 있습니다.

mjs? or js?

// index.mjs
import { odd, even } from "./utils.mjs";

function checkOddOrEven(num) {
  if (num % 2) {
    return odd;
  }

  return even;
}

export default checkOddorEven;

// utils.mjs
export const odd = "m 홀수";
export const even = "m 짝수";

// or
const odd = "m 홀수";
const even = "m 짝수";

export { odd, even };

  • ESM에서는 import, export, export default는 CJS와 달리 객체 혹은 함수가 아니라 문법 그 자체입니다.

Next.js, React 등과 같은 프레임워크 혹은 라이브러리를 사용하다보면, ESM을 활용하면서도 확장자 명이 .mjs가 아닌 .js인 것을 볼 수 있습니다.

다른 모듈 시스템을 사용하고 있는 것이 아니라, package.jsontype: 'module' 속성을 부여하면, .mjs라는 확장자 없이도, .js라는 확장자를 사용할 수 있게 됩니다.

type field의 기본값은 "commonjs" 이므로, .js는 CJS로 해석되므로, ESM 지원을 위해 type: 'module' 속성을 부여합니다.


4. What's the Dynamic Import?

Dynamic Import란 조건부(Conditional)로 Module을 불러오는 것을 의미합니다.

CJS

const a = false;

if(a){
	require('./func');
}

console.log('성공');

--- 결과 ---
// 성공

ESM

const a = false;

if(a){
	import './func.mjs';
}

console.log('성공');


--- 결과 ---
// SyntaxError : Unexpected String (import ~~)

위의 코드와 같이 ESMif 문 안에서 import 하는 것이 불가능합니다.



import function

  • ESM에서는 import라는 함수를 사용하여 모듈을 동적으로 불러올 수 있습니다.
  • import 함수는 Promise를 반환하기 때문에, await 혹은 then을 활용하여 Promise를 처리해야 합니다.

const a = true;

if (a) {
  const module1 = await import("./func.mjs");
  console.log(module1);

  const module2 = await import("./var.mjs");
  console.log(module2);
}

  • 위 코드에서는 async 함수를 사용하지 않았는데, 그 이유는 .mjs가 top-level-await를 지원하며, ESM의 최상위 스코프에서는 async 함수 없이도 await를 사용할 수 있습니다.

Reference

Node.js Module System