CommonJS와 ES-Module, 분명히 미래에는 ES-Module이 더 널리 쓰일 것이다.(적어도 난 그렇게 믿는다.)
하지만 분명 지금은 CommonJS가 더 널리 쓰이고, 새로운 프로젝트에서도 CJS를 지원하는 것은 당연하다.
따라서 내가 패키지를 만들고자 한다면 esm, cjs 모두를 지원하는 패키지를 작성하는 것이 좋다.
이때 esm, cjs를 모두 지원하는 패키지를 일명 dual package라고 한다고 한다.
이번에는 타입스크립트 코드를 컴파일해 esm, cjs를 모두 지원하는 패키지를 만들어 볼 것이다.
나는 이번 포스트에서
.cjs
와.mjs
를 상호 운용하는 것이 아닌 tsc 컴파일러를 이용해 하나의.ts
파일을.cjs
,.mjs
로 각각 2개 컴파일 할 것이다.위 방법은 아주 구버전 nodejs에서는 지원되지 않을 수도 있다.
문서를 확인한 바, LTS 12 버전 이후부터는 아마도 지원하는 것 같다. (정확하지는 않으니, 그냥 최신 버전 node에서는 동작한다고 여기면 될 것 같다.)
최종적으로 만들 패키지 구조는 아래와 같다.
.
├── cjs
│ ├── package.json
│ └── tsconfig.json
├── esm
│ ├── package.json
│ └── tsconfig.json
├── src
│ ├── utils
│ │ └── index.ts
│ ├── index.ts
│ └── helper.ts
├── tsconfig.json
└── package.json
src
, 타입스크립트 소스코드를 작성하는 곳우선 타입스크립트 코드는 앞으로 모두 src폴더 아래에 작성할 것이다.
위 코드에서는
3파일이 존재한다.
여기에 원하는 만큼 소스코드를 추가하면 된다.
해당 파일의 내용은 아래와 같다.
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./",
}
}
여기서 compilerOptions.module
은 타입스크립트 컴파일러로 commonjs 형식으로 내보내겠다는 의미이다.
extends
는 여기서 적히지 않은 컴파일러 설정은 부모 설정 파일의 내용을 따른다는 의미이다.
만약 ../tsconfig.json
에 compilerOptions.target
= es2020
이라는 부분이 있다면 이 ./cjs/tsconfig.json
역시 compilerOptions.target
이 es2020
이다. 왜냐하면 extends
로 확장된 설정이기 때문이다.
한마디로 클래스 상속과 유사하다.
해당 파일의 내용은 아래와 같다.
{
"type": "commonjs"
}
사실 이 파일은 중요해 보이지 않을 수도 있는데 매우 매우 중요한 파일이다.
이 파일이 있어야 해당 ./cjs
폴더 안의 js 파일들이 commonjs 형식임을 node에서 파악할 수 있다.
해당 파일의 내용은 아래와 같다.
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "ES2020",
"moduleResolution": "Node",
"outDir": "./",
}
}
여기서 compilerOptions.module
은 타입스크립트 컴파일러로 ES2020
형식으로 내보내겠다는 의미이다.
extends
는 위의 commonjs쪽 설명과 동일하다.
compilerOptions.moduleResolution
은 모듈 위치를 확인하는 방법을 어떻게 설정하는가에 대한 내용인데 너무 길어질 것 같으니 간단히 해당 설정의 영향만 이야기하면, 최신 Node에서 패키지를 분석하는 방법과 동일하게 typescript를 분석하라는 의미이다.
해당 파일의 내용은 아래와 같다.
{
"type": "module"
}
이 파일 역시도 매우 중요하다. 해당 모듈 아래에 있는 모든 js 파일들은 이제 es-module
로 인식된다.
{
"name": "test",
"version": "1.0.0",
"main": "./cjs/index.js",
"module": "./esm/index.js",
"exports": {
".": {
"import": "./esm/index.js",
"require": "./cjs/index.js"
}
},
"scripts": {
"build": "npm run build:cjs & npm run build:esm",
"build:cjs": "tsc --p ./cjs/tsconfig.json",
"build:esm": "tsc --p ./esm/tsconfig.json"
},
"devDependencies": {
"typescript": "^4.6.2"
}
}
여기서 script
, devDependencies
, name
, version
을 제외한 나머지
main
, module
, exports
는 매우 중요한 설정이다.
나는 해당 패키지의 모든 함수를 index.js
에 모아서 export 시켰기에 index.js만 내보내면 된다.
따라서 exports
에 .
(최상위 디렉터리를 import 하는 경우)만 넣어 줬다.
해당 설정에 의해 const test = require('test')
를 했을 때와 import test from 'test'
를 했을 때 각각 cjs
, esm
에서 다른 패키지를 가져오게 된다.
{
"include": [
"src/**/*.ts"
],
"compilerOptions": {
// "incremental": true,
"target": "ES2020",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
해당 설정은 typescript 설정 파일으로 별로 대단한게 없으니 넘어간다.
만약 빌드 속도를 올리려면 켜도 상관없는 옵션인 incremental
만 주석을 걸어놨다.
위의 사진은 esm 패키지를 이용했을때 어떤 결과가 나오는지에 대한 화면이다.
보다시피 package.json
에서 type
이 module
로 되어 있고 main.js가 잘 동작함을 볼 수 있다.
위의 사진은 cjs 패키지를 이용했을때 어떤 결과가 나오는지에 대한 화면이다.
보다시피 package.json
에서 type
이 commonjs
로 되어 있고 main.js에서 모듈 로더로 require
를 쓰는 것을 볼 수 있다.
그런데 이를 통해서 CJS와 ESM이 서로 다른 모듈을 실제로 불러오고 있는지 확인이 모호하니, 컴파일된 소스코드를 약간 손봐서 이 구분을 더 명확히 해 보겠다.
다음 줄들을 이렇게 수정해 보자.
// ./esm/helper.js
export const Helper = "esm:Helper";
// ./cjs/helper.js
export const Helper = "cjs:Helper";
이후 수정된 버전으로 다시 실험해 보면 다음과 같다.
보다시피 같은 test모듈을 받았음에도 현재 패키지를 불러오는 방식에 따라 esm, cjs를 다른 폴더에서 불러옴을 볼 수 있다.
js는 단언컨데 가장 큰 격변을 거친 언어라고 생각한다.
최초로는 클라이언트 사이드의 단순 스크립트 언어로 시작해 사실상의 웹 환경의 어셈블리 언어로 취급받기까지, 엄청난 환경의 변화와 수많은 프레임워크들이 존재했다고 해도 과언이 아니다.
그중 가장 큰 영향을 미친 것이라고 한다면 역시 cjs, commonjs일 것이다.
cjs는 초기 자바스크립트, 모듈도, 패키지도 없던 시절에는 이를 구현할 수 있게 해주는 유일한 희망이였다.
그러나 이제는 ES module이 있기에 이제는 서서히 보내줘야 하는 기술이다.
하지만 많은 사람들이 cjs와의 이별에 준비가 되지 않았으니 앞으로 수년은 dual-package로 모듈을 만드는 것은, 필수라고 생각한다.
해당 블로그에서 사용된 모든 소스코드는 아래 github에 있다.
Github : egoavara/blog-dualpackage
참고자료
- nodejs 모듈 생성에 대한 표준 문서
https://nodejs.org/api/packages.html#dual-commonjses-module-packages- typescript esm 모듈에 관한 문서
https://www.typescriptlang.org/docs/handbook/esm-node.html