본문 바로가기

프론트엔드

JS library에서 cjs, esm format 모두 지원하기

반응형
  • 프로젝트: loplat UI
  • 키워드: CommonJS, ES Modules, loplat UI, package.json, npm, babel, jest, rollup
  • 상황
    1. loplat UI는 tree shaking을 지원하기 위해 cjs format을 버리고 esm format의 빌드 결과를 채택했었다.(번들링 최적화를 통해 import cost 줄이기(1))

    2. 때문에 es6 문법을 기본적으로 이해하지 못하는 환경(Jest test, Next/SSR build)에서는 라이브러리를 es5 문법으로 transpile해야하는 번거로움이 있다.
      (Next.js는 'next-transpile-modules' https://www.npmjs.com/package/next-transpile-modules를 사용해야하고, jest도 추가 option을 설정해줘야한다. https://jestjs.io/docs/ecmascript-modules)

    3. 가장 좋은 것은 loplat UI 자체적으로 cjs format을 지원해주는 것이기 때문에, cjs와 esm format을 동시에 지원하고자 한다.
  • 해결 과정
    1. 앞선 라이브러리 번들링 용량 최적화 과정에서 수많은 시행착오를 겪었기에, rollup.config.js 에서 'format'의 값을 'cjs' OR 'esm'으로 설정하면 바로 원하는 format으로 빌드할 수 있는 상태이다.
        ...
      output: [
          {
            dir: '.',
            format: 'esm', // OR 'cjs'
            sourcemap: true,
          },
        ],
      ...


    2. 해야할 일은 esm/cjs format 둘 다 빌드하여 제공하고, customer project에서 원하는 형식의 결과물을 선택할 수 있도록 하는 것이다. 어떻게 해야할지 고민하다가 loplat UI와는 달리 es5 지원이 잘 되는 material-ui는 package.json이 어떻게 짜여있을 지 궁금해졌다.
      직접 material-ui를 설치한 뒤, node_modules의 package.json을 열어보았다.
        // @material-ui/core/package.json
      "sideEffects": false,
      ...
      "main": "./index.js",
      "module": "./esm/index.js",
      "typings": "./index.d.ts"
      
      
      // loplat-ui/package.json
      "main": "index.js",
      "module": "index.js",
      "types": "index.d.ts",
      "sideEffects": false,
      loplat UI와 다른 점은 바로 'module'이었다. material-ui는 index.js로 es5 format의 결과물(require)을 내보내고 있었고, esm/index.js에선 es6 format의 결과물(import/export)이 있었다.


    3. loplat UI에도 같은 방식을 적용하기 위해 rollup.config.js와 package.json을 수정한다.
        // rollup.config.js
      output: [
        {
          dir: '.',
          format: 'cjs',
          sourcemap: true,
        },
        {
          dir: './esm',
          format: 'esm',
          sourcemap: true,
        },
      ],
      ...
      
      // package.json
      "main": "index.js",
      "module": "esm/index.js",
      "types": "index.d.ts",
      "sideEffects": false,
      "files": [
        "esm",
        ...
      ],

      이 후 yarn build를 통해 빌드를 하면 다음과 같은 결과물이 나온다.



      index.js는 cjs format이고, esm/index.js는 es6 format이다.
        // index.js
      'use strict';
      
      Object.defineProperty(exports, '__esModule', { value: true });
      
      var black = require('./black-e8f8061e.js');
      var white = require('./white-b590e722.js');
      var blue = require('./blue-56febbdb.js');
      ...
      
      // esm/index.js
      export { b as black } from './black-8cbd8ffd.js';
      export { w as white } from './white-7f509504.js';
      export { g as blue, b as blue100, a as blue200, c as blue300, d as blue400, e as blue500, f as blue600 } from './blue-19cc5e55.js';
      ...
    4. 이제 이 패키지가 어떻게 작동하는지 테스트 해보기 위해 customer project에 loplat UI를 설치한다.
      테스트 결과를 요약하면 다음과 같다.
      • CSR, SSR 프로젝트 모두 에러 없이 빌드되며, tree-shaking도 동일하게 일어난다.(에러나 용량 증가 없음)
      • Next.js에서 next-transpile-modules configuration이 필요 없어졌다.
      • Jest에서 esm 지원을 위한 추가 설정이 필요 없어졌다.
      • npm 정보 패널에 보이는 Unpacked Size는 증가했지만(cjs 파일들이 추가되었기 때문), 압축된 용량이나 실제 사용시 용량은 변함이 없다.
        npm 정보 확인: https://www.npmjs.com/package/loplat-ui/v/1.5.1
        minifized 용량 확인: https://bundlephobia.com/package/loplat-ui@1.5.1

    5. 테스트 결과로 알 수 있는 사실은, ESM이 적합한 환경에서는 esm format이 사용되고 그렇지 않은 경우에는 cjs format이 사용된다는 것이었다.
      이것이 어떻게 가능한지 찾아보던 와중 rollup의 관련 문서를 찾을 수 있었다.

      https://github.com/rollup/rollup/wiki/pkg.module
      pkg.module will point to a module that has ES2015 module syntax but otherwise only syntax features that the target environments support
      .
      my-package continues to work for everyone using legacy module formats, but people using ES2015 module bundlers such as Rollup don't have to compromise. Everyone wins!

      pkg.main의 index.js는 legacy module format으로써 사용될 수 있지만, ESM이 사용될 수 있는 환경이라면 pka.module에 명시된 esm 모듈 역시 사용할 수 있는 것이다!
반응형