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
를 사용할 수 있습니다.