cjs, esm

김동현·2024년 4월 22일

회고

목록 보기
3/4
post-thumbnail

CommonJS, ESM

CommonJs는 JS 커뮤니티에서 채택된 최초의 모듈 시스템 중 하나이다.
처음에는 Nodejs와 함께 사용하기 위해 서버 측 개발을 염두해두고 설계 되었다고 한다.

The project was started by Mozilla engineer Kevin Dangoor in January, 2009 and initially named ServerJS
이 프로젝트의 초기 이름은 ServerJS였다.
하지만, API의 폭넓은 적용 가능성을 표현하기 위해 CommonJS로 변경했다고 한다. :)

✅ 즉, CommonJS는 JS가 모듈을 관리하는 최초의 방법론인 것이다.

그와 달리 ESModule의 ESM은 ECMAScript에서 지원하는 방식이며, Typescript, 최신 ES6를 지원하는 JS에서는 ESM 방식을 사용한다.

📝 CommonJS와 ESM은 서로 호환이 되지 않는 다는 것을 일단 기억하고 가자

Typescript + exports

서로 다른 모듈을 사용하기 위한 해결 방법 첫 레퍼런스는 How to Create a Hybrid NPM Module for ESM and CommonJStypescript를 활용하여 두 모듈을 모두 컴파일한 후 해결하는 방법이었다.

🤔 레퍼런스에서 fixup이라는 Shell스크립트가 왜 필요한 지 궁금했다.

✅ 이는, Nodejs가 모듈을 구분하기 위해 필요한 과정으로 위 레퍼런스를 따라 스크립트를 실행하고 나면 위와 같이 각 모듈마다 package.json이 생성되고, 정상적으로 cjs, ems 모듈 모두 라이브러리를 사용할 수 있게 된다.

📌 어떻게 Nodejs가 모듈을 구분하는 지 알아보자 :)

Nodejs가 모듈을 관리하는 방식이 CommonJS임을 구분하는 방법

  1. 파일 확장자가 .cjs로 되어 있는 경우
  2. 파일 확장자가 .js로 되어 있고 가까운 부모의 package.jsontype의 필드값이 commonjs인 경우
  3. 파일 확장자가 .js로 되어 있고 가까운 부모의 package.jsontype의 필드값이 명시되어 있지 않는 경우
  4. 파일 확장자가 .mjs, .cjs, .json, .node, .js가 아닌 경우
    • 이 경우 가장 가까운 부모의 package.json의 type이 module로 되어 있더라도 파일 내부에 require()로 모듈을 불러온다면 commonjs로 인식된다.
  5. 모듈이 require()로 호출되는 경우 내부 파일과 상관없이 무조건 commonjs로 인식한다.

💡 여기서 중요한 것은 Default값이 Commonjs라는 것이다.
-> package.json 또는 파일 확장자에 별다른 플래그를 주지 않는 다면 commonjs를 바라보게 된다.

이렇게 기본값이 설정되어 있는 이유는 CommonJS와 ESM간 호환이 되지 않고, 이미 많은 패키지가 commonjs를 기반으로 제작되어 있기 때문이라고 한다.

Determining module system Nodejs의 Docs를 읽어봐도 좋을 것 같다.

📌 노드에게 각 모듈에 접근하기 위한 경로를 알려주기 위해 쉘 스크립트를 활용하여 상위 경로에 package.json을 만들어 준 것이었다!

Package.json Exports 필드

앞에서 ESM과 CommonJS는 호환될 수 없는 모듈이라고 했다.
그렇다면, 내 프로젝트가 CommonJS라면 ESM모듈로 구현된 라이브러리는 사용할 수 없고, 반대로 ESM 모듈 방식이라면 CommonJS 모듈 방식을 사용할 수 없을 것이다.

🤔 그럼 어떻게 이와 같은 문제를 해결하고 있을까?

✅ 일단, 요즘 대부분의 라이브러리들이 CommonJS와 ESM방식을 동시에 지원하고 있다.

더해서, 우리는 package.json에서 제공하는 exports 필드를 활용해 내 프로젝트가 사용하고 있는 모듈에 따라 분기 처리하여 올바른 모듈을 불러온다.

기존에는 main 프로퍼티를 통해 라이브러리를 접근 했지만, 하나의 프로퍼티에 ESM, CommonJS 접근 경로를 모두 표기할 수 없으니 exports 경로가 추가된 것이다.
-> exports 경로가 있다면, main, module, types 프로퍼티 보다 더 우선 순위가 높게 평가 된다.

📌 exports는 상대 경로로 작성해야 한다.
.은 해당 라이브러리를 의미하고 추가적인 상대 경로를 결합하여 적절한 경로로 접근하게 도와준다.

"exports": {
    ".": {
      "require": "./dist/index.js", // commonjs 모듈
      "import": "./dist/index.mjs", // esm 모듈
      "types": "./dist/index.d.ts"  // typescript에서 타입을 불러올 떄
      "browser": "" // 브라우저에서 사용되는 경우
      "default": "" // 그외
    }
  },

즉, 아래와 같은 과정을 통해 올바른 프로젝트의 모듈을 가져오는 것이다.

  1. 우리가 import {} from "";, const something = require() 따위의 방식으로 라이브러리 모듈을 불러 온다.
  2. Node는 Node_module 내 라이브러리 폴더를 찾는다.
  3. 라이브러리의 package.jsonexports 프로퍼티를 보고 모듈이 있는 경로로 이동한다.
    • . 상대 경로는 라이브러리 폴더 경로 기준이다.
  4. 모듈을 가져온다!



Node Docs의 Conditional Exports 컨텐츠내에 정의되어 있는 각 프로퍼티 별 내용을 참고해봐도 좋을 것 같다.

tsup

tsup 추가적인 환경 설정 없이 esbuild를 통해 Typescript Library를 번들로 제공해 주는 라이브러리이다!

  1. npm i -D tsup를 통해 tsup를 설치하자.
  2. tsup src/index.ts --format cjs,esm
    • tsup 뒤에 진입점(Entry) 경로를 입력해준 뒤 제공하고 싶은 모듈 포맷을 입력해 주자.
  3. package.json에 exports 경로를 지정한다.
// package.json
{
  // ...
    "scripts": {
    "build": "tsup lib/index.ts --format cjs,esm --dts --clean",
    "test": "jest",
    "ts-node": "ts-node",
    "prepare": "yarn build"
  },

  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",

  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  },
  // ...
}

yarn build CLI를 입력하면 아래 이미지와 같이 번들링 된 파일이 생성된다.

이제 우리의 라이브러리는 ESM, CommonJS 모듈을 모두 지원하게 되었다!!

tsup 실행 환경을 조성하거나, 더 많은 정보를 얻고 싶다면 tsup Docs를 한 번 살펴 보면 좋을 것 같다 😀

참고자료

profile
달려보자

0개의 댓글