본문 바로가기

프론트엔드

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

  • 프로젝트: loplat UI
  • 키워드: bundling optimization, rollup, tree shaking, import cost, webpack, side effects, postpublish
  • 상황
    1. 이전 포스트 번들링 최적화를 통해 import cost 줄이기(1)에서 'esm format build'와 'code splitting' 단계를 거쳤지만 여전히 tree shaking이 제대로 되지 않는다.
  • 해결 과정
    1. rollup 공식 사이트에서 tree shaking 이 작동하지 않는 상황에 대한 문서를 확인했다.(https://rollupjs.org/guide/en/#tree-shaking-doesnt-seem-to-be-working)
      Sometimes, you'll end up with code in your bundle that doesn't seem like it should be there.  ... 중략 ...
      Rollup's static analysis will improve over time, but it will never be perfect in all cases – that's just JavaScript.
      But Rollup has to be conservative about what code it removes in order to guarantee that the end result will run correctly. If an imported module appears to have side-effects, either on bits of the module that you're using or on the global environment, Rollup plays it safe and includes those side-effects.
    2. 동적 언어인 자바스크립트를 정적으로 분석하는 것은 매우 힘든 작업이기 때문에, sideEffects가 있을 수 있는 경우 트리쉐이킹을 완벽하게 지원하기는 어렵다(최대한 safe하게 작동한다)는 것이다.
      하지만 UI 라이브러리 프로젝트는 컴포넌트별로 독립적이기 때문에 서로 sideEffects가 없는 상황이다. 따라서 sideEffects가 없을 것이라고 개발자가 명시해주어 이러한 상황을 피해야한다.
    3. webpack 공식 문서를 통해 package.json 파일에서 'sideEffects' 필드를 추가하면 sideEffects의 영향을 받지 않겠다고 명시할 수 있음을 확인했다.(https://webpack.js.org/guides/tree-shaking/)
      "sideEffects" package.json property to denote which files in your project are "pure" and therefore safe to prune if unused.
        // package.json 
      {
        "name": "loplat-ui",
        "version": "1.2.1",
        "main": "index.js",
        "module": "index.js",
        "types": "index.d.ts",
        "sideEffects": false,
        ...
      }

      package.json에 "sideEffects": false를 추가한다.
    4. 이제 실제 리액트 프로젝트에서 import시, tree shaking이 제대로 지원되는 것을 확인했다!
      여기서 주의할 점은 sideEffects 라는 속성은 rollup.config.js가 아닌 package.json에 설정한 값이라는 것이다. 즉, 빌드 결과물과는 관련이 없으며 라이브러리를 사용하는 프로젝트에서 import를 할 때 어떻게 tree shaking을 하겠느냐에 대한 설정이다.
      따라서, 라이브러리를 사용하는 프로젝트에서 node_modules/loplat-ui/package.json 에 들어가 강제로 "sideEffects" 필드를 지우면 더이상 tree shaking이 일어나지 않는다.
    5. tree shaking이 안되는 버전(v1.2.0)에서 새로 배포한 v.1.2.1을 적용했더니, 리액트 프로젝트(loplat x mobile) 번들링 용량이 200KB가량 감소했다.
      v1.2.0(왼쪽) vs v1.2.1(오른쪽, tree shaking 적용)
  • 한 걸음 더 - 상황
    1. 보통 라이브러리를 설치하면 index.js를 제외한 (code splitting된) 파일들은 'dist'나 'public' 파일 안에 저장되어있다.
      이 때, 특정 컴포넌트를 불러오고 싶다면 아래와 같이 import 할 것이다.
        import { Col } from 'loplat-ui';
      import { Col } from 'loplat-ui/dist/core/layout/Col';

      보통의 경우 첫째 줄과 같이 import하기 때문에 문제가 없겠지만, 둘째 줄로 import하는 경우엔 'dist'라는 파일 경로가 보기에 좋지 않아보였다.
  • 한 걸음 더 - 해결방법
    1. rollup.config.js의 output 경로는 보통 'dist'로 설정되어있지만, loplat-ui 에서는 '.'으로 설정한다.
        export default {
        input: ['src/**/index.ts', 'src/**/index.tsx', 'src/**/generated/*.tsx'],
        output: [
          {
            dir: '.',
            format: 'esm',
            sourcemap: true,
          },
        ],
        ...​
    2. 이 상태로 build를 하게되면, dist 폴더가 생기지 않고 root directory 바로 아래에 빌드 결과물이 쌓인다.
      빌드 결과물(src 등 제외)
    3. 이 상태로 npm publish를 하면 'dist'와 같은 경로를 거치지 않고 바로 import할 수 있다.
        import { Col } from 'loplat-ui/core/layout/Col';
       
    4. 다만 빌드 결과 파일로 인해 프로젝트 폴더가 지저분해보이는 문제가 있었다. 그래서 'clean-build-files.sh'라는 스크립트 파일을 생성한 뒤, npm publish 후 자동 실행되도록 설정했다.
        // clean-build-files.sh
      rm -rf assets
      rm -rf components
      rm -rf core
      rm -rf formControls
      rm $(find *.js -not -name rollup.config.js)
      rm *.js.map
      rm index.d.ts
      
      // package.json의 scripts 명령어
      "postpublish": "sh clean-build-files.sh",
      이제 빌드 후 배포에 성공하면 빌드 결과물들이 자동 삭제되어 프로젝트 폴더를 깔끔하게 유지할 수 있다!