pnpm init 명령어 실행 후 index.js 파일에서 작업을 하다 보면 라이브러리나 다른 파일을 import를 했을 때 package.json의 type을 module로 설정하라는 오류가 뜬다.
오늘은 package.json에서 type의 값에 따라 모듈 시스템을 어떻게 인식하는지, 그리고 각각의 차이점은 무엇인지에 대해 알아보겠다. 시간이 없다면 요약만 봐도 좋다.
{
"type": "commonjs"
}
index.js
package.json
package.json에 있는 type의 기본 값은 commonjs이다. 이때는 CommonJS 모듈 시스템으로 인식하고 파일명을 index.js로 지어도 index.cjs로 인식하게 된다.
반면 type의 값을 module로 설정하게 되면 ESM 모듈 시스템으로 인식하고 파일명을 index.mjs로 인식하게 된다.
그렇다면 CommonJS와 ESM의 동작 방식은 어떨까?
// math.cjs
module.exports.math = {
add(a, b) {
return a + b;
},
};
// index.cjs
const { math } = require("./math.cjs");
console.log(math.add(1, 2)); // 3
require를 사용하여 모듈을 불러오고 module.exports를 이용하여 모듈을 내보낸다.
또한 동적으로 모듈을 가져올 수 있다.
// add.cjs
module.exports = {
add(a, b) {
return a + b;
},
};
// index.cjs
const fileName = "add.cjs";
const { add } = require(`./${fileName}`);
console.log(add(1, 2)); // 3
이는 CJS가 런타임에서 모듈 관계를 파악하기 때문이다.
// math.mjs
export function add(a, b) {
return a + b;
}
// index.mjs
import { add } from "./math.mjs";
console.log(add(1, 2));
import를 사용하여 모듈을 불러오고 export를 이용하여 모듈을 내보낸다.
CJS와는 다르게 ESM은 컴파일 타임에 분석(정적 분석)되기 때문에 동적으로 모듈을 가져오지 못한다. 하지만 정적 분석을 하기 때문에 Tree-shaking을 할 수 있어 자바스크립트 번들의 크기는 줄어든다.
또한 ESM에서는 CJS를 import 할 수 있지만 CJS에서는 ESM을 require 할 수 없다. 이는 ESM만 Top-level Await를 지원하기 때문이다.
| 분석 시점 | Tree-shaking 여부 | 동작 | 모듈 가져오기/내보내기 | |
|---|---|---|---|---|
| CJS | 런타임 타임 | O | 동기 | require/module.exports |
| ESM | 컴파일 타임 | X | 비동기 | import/export |
오류가 나면 그냥 무심코 package.json의 type을 지정해 주고 넘어갔었는데, 이번 기회에 둘의 차이에 대해 공부해 보는 계기가 되어 좋았다. 토스의 글이 정말 잘 정리되어 있으니 한 번씩 읽어보는 것을 추천한다.