typescript npm패키지를 dual package로 만들기.

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폴더 아래에 작성할 것이다.
위 코드에서는

  • utils/index.ts
  • index.ts
  • helper.ts

3파일이 존재한다.
여기에 원하는 만큼 소스코드를 추가하면 된다.

cjs, commonjs 관련 패키지가 컴파일 되는 곳.

./cjs/tsconfig.json

해당 파일의 내용은 아래와 같다.

{
    "extends": "../tsconfig.json",
    "compilerOptions": {
        "module": "CommonJS",
        "outDir": "./",
    }
}

여기서 compilerOptions.module은 타입스크립트 컴파일러로 commonjs 형식으로 내보내겠다는 의미이다.
extends는 여기서 적히지 않은 컴파일러 설정은 부모 설정 파일의 내용을 따른다는 의미이다.

만약 ../tsconfig.jsoncompilerOptions.target = es2020이라는 부분이 있다면 이 ./cjs/tsconfig.json역시 compilerOptions.targetes2020이다. 왜냐하면 extends로 확장된 설정이기 때문이다.

한마디로 클래스 상속과 유사하다.

./cjs/package.json

해당 파일의 내용은 아래와 같다.

{
  "type": "commonjs"
}

사실 이 파일은 중요해 보이지 않을 수도 있는데 매우 매우 중요한 파일이다.
이 파일이 있어야 해당 ./cjs 폴더 안의 js 파일들이 commonjs 형식임을 node에서 파악할 수 있다.

esm, es-module 관련 패키지가 컴파일되는 곳

./esm/tsconfig.json

해당 파일의 내용은 아래와 같다.

{
    "extends": "../tsconfig.json",
    "compilerOptions": {
        "module": "ES2020",
        "moduleResolution": "Node",
        "outDir": "./",
    }
}

여기서 compilerOptions.module은 타입스크립트 컴파일러로 ES2020 형식으로 내보내겠다는 의미이다.

extends는 위의 commonjs쪽 설명과 동일하다.

compilerOptions.moduleResolution은 모듈 위치를 확인하는 방법을 어떻게 설정하는가에 대한 내용인데 너무 길어질 것 같으니 간단히 해당 설정의 영향만 이야기하면, 최신 Node에서 패키지를 분석하는 방법과 동일하게 typescript를 분석하라는 의미이다.

./esm/package.json

해당 파일의 내용은 아래와 같다.

{
  "type": "module"
}

이 파일 역시도 매우 중요하다. 해당 모듈 아래에 있는 모든 js 파일들은 이제 es-module로 인식된다.

package, tsconfig

./package.json

{
  "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에서 다른 패키지를 가져오게 된다.

./tsconfig.json

{
    "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


위의 사진은 esm 패키지를 이용했을때 어떤 결과가 나오는지에 대한 화면이다.

보다시피 package.json에서 typemodule로 되어 있고 main.js가 잘 동작함을 볼 수 있다.

CJS


위의 사진은 cjs 패키지를 이용했을때 어떤 결과가 나오는지에 대한 화면이다.

보다시피 package.json에서 typecommonjs로 되어 있고 main.js에서 모듈 로더로 require를 쓰는 것을 볼 수 있다.

그런데 이를 통해서 CJS와 ESM이 서로 다른 모듈을 실제로 불러오고 있는지 확인이 모호하니, 컴파일된 소스코드를 약간 손봐서 이 구분을 더 명확히 해 보겠다.

다음 줄들을 이렇게 수정해 보자.

// ./esm/helper.js
export const Helper = "esm:Helper";

// ./cjs/helper.js
export const Helper = "cjs:Helper";

이후 수정된 버전으로 다시 실험해 보면 다음과 같다.

ESM, 수정 후 버전

CJS, 수정 후 버전

보다시피 같은 test모듈을 받았음에도 현재 패키지를 불러오는 방식에 따라 esm, cjs를 다른 폴더에서 불러옴을 볼 수 있다.


js는 단언컨데 가장 큰 격변을 거친 언어라고 생각한다.

최초로는 클라이언트 사이드의 단순 스크립트 언어로 시작해 사실상의 웹 환경의 어셈블리 언어로 취급받기까지, 엄청난 환경의 변화와 수많은 프레임워크들이 존재했다고 해도 과언이 아니다.

그중 가장 큰 영향을 미친 것이라고 한다면 역시 cjs, commonjs일 것이다.

cjs는 초기 자바스크립트, 모듈도, 패키지도 없던 시절에는 이를 구현할 수 있게 해주는 유일한 희망이였다.

그러나 이제는 ES module이 있기에 이제는 서서히 보내줘야 하는 기술이다.

하지만 많은 사람들이 cjs와의 이별에 준비가 되지 않았으니 앞으로 수년은 dual-package로 모듈을 만드는 것은, 필수라고 생각한다.

해당 블로그에서 사용된 모든 소스코드는 아래 github에 있다.
Github : egoavara/blog-dualpackage

참고자료

좋은 웹페이지 즐겨찾기