본문 바로가기

프론트엔드

번들링 최적화를 통해 import cost 줄이기(1)

  • 프로젝트: loplat UI
  • 키워드: bundling optimization, rollup, tree shaking, code splitting, commonJS, es modules, import cost
  • 상황
    1. 팀원분이 완성해둔 loplat ui 라이브러리를 실제 프로젝트에서 사용하고자한다.
    2. 아래와 같이 사용하고 싶은 컴포넌트를 import 한다.
        import { Button } from 'loplat-ui';
      import { black400 } from 'loplat-ui';
    3. 사용할 컴포넌트에 해당하는 코드만 불러오는 것이 아니라, 번들링 된 코드를 모두 불러와 과도한 import cost가 든다.
      black400이라는 palette const를 불러오는 것이므로 1kB도 되지않는 작은 크기여야만 한다.
      import cost 가 과도한 상황
  • 해결과정
    1. 기존 rollup.config.js 파일을 보고 문제의 원인을 찾아보았다.
        export default {
        input: "src/index.ts",
        output: [
          {
            file: packageJson.main,
            format: "cjs",
            sourcemap: true
          },
          {
            file: packageJson.module,
            format: "esm",
            sourcemap: true
          }
        ],
        plugins: [
          peerDepsExternal(),
          resolve(),
          commonjs(),
          typescript({ useTsconfigDeclarationDir: true }),
          postcss({
            extensions: ['.css']
          })
        ]
      };​

      결과물로 나오는 index.js가 cjs format, 즉 commonJS 모듈 방식의 파일이었다. 사내의 리액트 프로젝트에서 cjs 포맷이 필요한 경우는 없기 때문에 es module 방식인 'esm' format만 남기고 제거하였다.
        if (process.env.NODE_ENV === 'production') {
        require('./production.js')
      } else {
        require('./development.js')
      }​

      commonJS는 코드를 불러오는 require 문이 동기적으로 발생한다. 따라서 위와 같은 코드를 번들링한다고 가정했을 때, 어떤 파일을 필요로할지 정적으로 판단할 수 없으므로 production.js와 development.js를 모두 포함한 번들링 결과가 만들어진다.
      이는 commonJS에서 기본적으로 tree-shaking이 불가능한 이유이다. 반대로 es modules 에서는 파일의 맨 위에서 import 해야만 하기 때문에 정적으로 코드를 분석할 수 있다.
        import { Button } from 'loplat-ui';
      import { subtract } from 'lodash-es';
    2. esm format만 남기고 빌드한 결과이다. 빌드 결과물인 index.js를 살펴보면
        // es modules
      ...
      var Add = React__default.memo(function (_a) {
          var _b = _a.size, size = _b === void 0 ? 18 : _b, _c = _a.fillColor, fillColor = _c === void 0 ? '#9DAAB7' : _c, className = _a.className, style = _a.style;
          var uniqueId = String(Math.random().toString(36).substr(2, 9));
          return ...;
      });
      ...
      
      export { Add as AddIcon, Alert as AlertIcon, ... }
      
      // commonJS
      // exports.AddIcon = Add;
      // exports.AlertIcon = Alert;
       exports 를 이용하는 cjs 결과와는 달리 export 를 이용하여 컴포넌트를 내보내는 것을 확인할 수 있다.

    3. 하지만 여전히 tree shaking 이 되지 않는 것이 확인되었다.
    4. index.js 라는 하나의 파일에 모든 라이브러리 코드가 들어있어, tree shaking을 하기엔 아직 힌트(hint)가 부족하다고 판단했다. 그래서 컴포넌트마다 output을 번들링하여 결과물을 쪼갰다.
        import multiInput from 'rollup-plugin-multi-input';
      ...
      export default {
        // NOTE: tree shaking 을 위해 esm 파일들을 code splitting 하여 빌드한다.
        input: ['src/**/index.ts', 'src/**/index.tsx', 'src/**/generated/*.tsx'],
        output: [
          {
            dir: 'dist',
            format: 'esm',
            sourcemap: true,
          },
        ],
        ...​

      이때, 'rollup-plugin-multi-input' 이라는 플러그인을 이용하여 input에 모든 컴포넌트를 넘겨주었다.

    5. 아래는 번들링 결과이다.
      index.js에서 전체 컴포넌트에 대한 모든 로직과 코드(2번에서 var Add = ...)가 있는 것이 아니라, 각각의 컴포넌트로 쪼개진 js 파일을 단순히 export함을 알 수 있다.
      즉 import { Button } from 'loplat-ui'; 라고 했을 때, components/Button/index.js 에 해당하는 코드만 가져온다면 원하는대로 tree-shaking이 될 것이라 기대했다.

    6. 하지만 결과는 여전히 실패였다.
      다음글 '번들링 최적화를 통해 import cost 줄이기(2)'에서 tree-shaking 지원을 위한 마지막 단계를 알아보자.