Webpack에서 Tree Shaking 적용하기

Daybrush (Younkue Choi)
NAVER FE Platform
Published in
10 min readOct 1, 2018

egjsInfiniteGrid 컴퍼넌트에 용량 개선 작업 중에 Tree Shaking을 도입하면서 알게된 사실을 정리해봤습니다.

환경은 webpack 3.x, 4.x | babel-loader 8.x | babel 7.x에서 테스트했습니다.

npm install babel-loader @babel/core @babel/preset-env webpack

Tree Shaking?

Tree Shaking은 사용하지 않는 코드를 제거함으로써 용량을 줄일 수 있는 방식입니다.

특정 라이브러리를 참조하면 참조한 라이브러리의 크기만큼 최종 번들의 용량이 증가하게 됩니다. 하지만 특정 라이브러리의 모든 코드를 쓰지 않습니다.

  • 그림과 같이 APP은 C의 2번만을 사용하고 있습니다. 그렇다면 C의 1번과 3번을 코드에 포함할 필요가 있을까요?
  • 또한 C의 2번은 B의 3번, B의 3번은 A의 3번을 참조하고 있습니다. 그렇다면 B의 1번, 2번 A의 1번, 2번은 코드에 포함할 필요가 있을까요?

Tree Shaking은 Bundler(webpack, rollup)와 UglifyJS가 역할을 담당 하고 있습니다.

  • Bundler의 역할은 사용하지 않는 모듈에 사용하지 않았다는 주석과 함께 export를 제거합니다.
    (rollup webpack4(sideEffects 활성화된 경우)에서는 사용하지 않는 모듈을 직접 제거합니다.)
  • UglifyJS는 Bundler를 통해 export가 제거된 경우 모듈은 dead code가 되어 제거됩니다.

리액트 컴퍼넌트에서 InfiniteGrid 사용해서 빌드한 사진입니다.

  • Tree Shaking 적용 전
  • Tree Shaking 적용 후

Tree Shaking을 적용해보면 좋을 것 같은 대상

모듈이 분리 되어 있고 사용자는 특정 모듈만 사용하는 경우

  • InfiniteGrid 같은 경우 5가지 layout을 제공하고 있고 사용자는 그 중 1개만 사용하기 때문에 다른 4개를 사용할 필요가 없습니다.
  • react-infinitegrid는 InfiniteGrid의 코어(InfiniteGrid의 2/5 크기)를 사용하지 않습니다.
  • view360경우에도 2개 뷰어중 1개만 사용할 수 있고 Axes도 마찬가지로 4개의 Input을 사용하고 1개만 사용하는 경우라면 3개를 사용하지 않게 됩니다.

Tree Shaking을 어떻게 적용할까?

라이브러리를 제공하는 쪽에서 설정을 해야 합니다. (단, 사용자의 bundler에 따라 결과가 다를 수 있습니다.)

require가 아닌 import / module.export(s)가 아닌 export default와 export로 이루어져 있어야 합니다.
the static structure of ES2015 module syntax, i.e. import and export

package.json에 module필드를 사용합니다.

"main": "dist/infinitegrid.js",
"module": "dist/esm/",

.babelrc preset에 “modules”: false로 지정합니다.

modules를 false로 하면 import, exportrequire, module.exports로 바뀌지 않습니다. (Tree Shaking 조건)

  • .babelrc 적용전
{
“presets”: [
[
@babel/preset-env”,
{
“loose”: true
}
]
]
}
  • .babelrc 적용후
{
"presets": [
[
"@babel/preset-env",
{
"modules": false,
"loose": true
}
]
]
}

Tree Shaking이 적용되지 않는 경우

사용하지 않더라도 코드에 포함되는 경우가 있습니다. 사용하지 않지만 다른 코드에 영향을 끼칠 수 있다고 판단하는데 이런 경우를 Side Effect가 발생했다고 합니다.

  1. 전역함수를 사용하는 경우 (Object, Math, String, RegExp)
export const isMobile = /mobi|ios|android/.test(agent);

2. 함수 실행 코드에서 멤버변수를 변경하고 반환하는 경우

3. static class properties를 사용하는 경우 (babel)
https://babeljs.io/docs/en/next/babel-plugin-proposal-class-properties.html
static method는 side effect가 발생하지 않습니다.

그래서 babel플러그인을 직접 만들어 해결했습니다. (babel-plugin-no-side-effect-class-properties)

4. Class를 사용하는 경우 (babel < 7)
babel 7 부터는 class를 babel로 변환시 /*#__PURE__*/라는 라벨이 붙습니다.

/*#__PURE__*/ 라벨이 붙으면 Side Effect가 일어나지 않는 영역으로 간주되기 때문에 prototype을 사용하더라도 Side Effect가 발생하지 않습니다.
https://babeljs.io/blog/2017/09/12/planning-for-7.0#pure-annotation-in-specific-transforms-for-minifiers

babel 7 이전 버전에서는 플러그인으로 지원이 가능합니다.
https://www.npmjs.com/package/babel-plugin-annotate-pure-calls

5. import한 라이브러리를 사용한 함수를 사용하지 않는 경우(webpack 3)

  • b를 사용하지 않으면 b는 제거되더라도 b안에 a가 있기 때문에 a는 side effect가 뜨면서 제거되지 않습니다.

6. import한 라이브러리를 다시 export default …한 경우(webpack 3)

  • export default할 경우 사용하지 않더라도 참조되는 코드가 추가됩니다.
  • export {… as default}로 바꾸면 가능합니다.

Side Effect가 일어나는 코드지만 발생하지 않게 하는 방법

Side Effect가 발생하지만 실제로는 영향을 끼치지 않는 코드일 수 있습니다.

  • 다른 코드 또는 외부에 영향을 끼치지 않는다고 자신할 수 있는 코드라면 side effect가 없다고 package.json에 명시해줍니다. (webpack4)
{
// side effect가 전혀 없는 경우
"sideEffects": false,
// side effect가 일부만 있는 경우
"sideEffects": ["./dist/....", "./dist/...."]
}

또한 /*#__PURE__*/라벨을 직접 붙이는 방법이 있습니다.

  • /*#__PURE__*/라벨을 붙이면 Side Effect가 일어나지 않는다고 판단합니다.

강제로 Side Effect 발생하는 방법

VERSION과 같이 무조건 포함되어야 하는 요소들은 사용하지 않더라도 있어야 합니다.

  • 파일을 만들어 export default 대신 module.exports을 사용하여 Tree Shaking이 되지 않게 합니다.
module.exports = "#__VERSION__#";

typescript환경에서 적용하는 방법

typescript는 tsc로 transpile하면 Class는 /** @class */라는 라벨이 붙습니다.

UglifyJS는 /** @class */라벨을 지원하지 않기 때문에 /*#__PURE__*/라벨을 수동으로 붙혀줘야 합니다.

  • replace-in-file 모듈을 써서 라벨을 바꾸는 스크립트입니다.
"scripts": {
"replace": "replace-in-file '/** @class */' '/*#__PURE__*/' dist/*/*.js"
}
$ npm run replace

결과

13개의 모듈 중 크기가 작은 모듈 1개만 사용해서 빌드를 했습니다.

import {GridLayout} from “@egjs/infinitegrid”;GridLayout();
  • es3 형식의 bundle을 가리켰을 경우 tree shaking이 적용되지 않기 떄문에 bundle의 용량만큼 늘어납니다.
  • webpack3을 사용하여 es모듈을 가르킨 경우 전혀 참조 되지 않는 모듈만 제거됩니다.
  • webpack3에서 es모듈 중 import하고 export default된 경우 export {default as …}로 바꾸면 Tree Shaking이 적용됩니다.
  • webpack4에서는 참조되더라도 사용하지 않으면 전부 제거됩니다. 하지만 side effect가 일어나는 것은 제거가 안됩니다.
  • webpack4를 사용하고 package.json에서 “sideEffects”: false를 설정하면 side effect가 일어나도 사용하지 않으면 제거됩니다.

위의 스크린샷 처럼 Tree Shaking 을 통해 사용하지 않는 코드를 제거함으로써 51kb 에서 4kb 까지 용량을 줄일 수 있었습니다. 저희의 경험이 웹팩 환경에서 용량 절감을 위해 고민하고 계신 분들께도 조금이라도 참고가 되셨으면 좋겠습니다.

--

--