Server-Side Rendering 그리고 Node.js를 활용한 Server를 구축하기 시작하면서, 모듈 시스템에 대한 이해도가 부족한다는 점을 깨달았습니다.
특히, ECMAScript가 표준이 되기 이전부터 CommonJS가 널리 사용됨에 따라, 다양한 패키지를 설치하고 사용하는 과정에서 require 문과 import 문이 혼용되어 사용되는 일종의 문제를 정의하기 위해 정리하기 시작하였습니다.
Node.js 12부터 ECMAScript Modules 라는 새로운 모듈 시스템이 추가되고 이후 node 진영의 표준이 되면서, 두 모듈 시스템에 잘 대응하는 방법을 알아야 합니다.
아래의 글에서
CommonJS는CJS,ECMAScript Modules는ESM이라고 부르겠습니다.
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에서ESM을import할 수 없습니다.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 -> {}
exports와 module.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.json에type: '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 ~~)
위의 코드와 같이
ESM은if문 안에서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를 사용할 수 있습니다.