Tree Shaking이란

Tree Shaking은 사용하지 않는 코드를 번들에서 제거하는 최적화 기법이다. 나무를 흔들면 죽은 잎이 떨어지듯, 번들에서 죽은 코드(dead code)를 털어낸다는 의미다.

왜 필요한가

lodash 같은 유틸리티 라이브러리를 생각해보자.

import { debounce } from 'lodash';

debounce 하나만 쓰는데 lodash 전체가 번들에 포함되면 낭비다. Tree Shaking이 제대로 동작하면 실제로 사용하는 debounce 함수만 번들에 포함된다.

동작 원리

Tree Shaking은 ES6 모듈의 정적 구조에 의존한다.

// ES6 모듈 - import/export가 정적으로 분석 가능
import { a } from './module';

// CommonJS - 런타임에 결정되므로 정적 분석 불가
const { a } = require('./module');

ES6의 import/export는 파일 최상단에만 올 수 있고, 조건문 안에 넣을 수 없다. 덕분에 번들러가 코드를 실행하지 않고도 어떤 모듈이 사용되는지 파악할 수 있다.

적용 조건

Tree Shaking이 제대로 동작하려면 몇 가지 조건이 필요하다.

1. ES6 모듈 사용

CommonJS(require)가 아닌 ES6 모듈(import/export)을 써야 한다.

// Good
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;

// Bad - 전체가 하나의 객체로 export됨
module.exports = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

2. sideEffects 설정

package.jsonsideEffects 필드를 설정하면 번들러에게 힌트를 줄 수 있다.

{
  "sideEffects": false
}

false로 설정하면 "이 패키지의 모든 모듈은 부수 효과가 없다"는 의미다. 번들러가 더 공격적으로 코드를 제거할 수 있다.

CSS 파일처럼 import만 해도 효과가 있는 파일은 예외로 지정한다.

{
  "sideEffects": ["*.css", "*.scss"]
}

3. production 모드

Webpack에서는 mode: 'production'일 때 Tree Shaking이 활성화된다.

module.exports = {
  mode: 'production'
};

development 모드에서는 디버깅 편의를 위해 비활성화되어 있다.

주의할 점

부수 효과가 있는 코드

// utils.js
export const log = (msg) => console.log(msg);

log('모듈 로드됨');  // 부수 효과 - import만 해도 실행됨

export const add = (a, b) => a + b;

이런 코드는 add만 import해도 log('모듈 로드됨')이 실행된다. 번들러 입장에서는 이 코드를 제거해도 되는지 판단하기 어렵다.

클래스는 제거가 어렵다

class Utils {
  static add(a, b) { return a + b; }
  static subtract(a, b) { return a - b; }
}

클래스는 하나의 덩어리라서 메서드 단위로 제거하기 어렵다. 개별 함수로 export하는 게 Tree Shaking에 유리하다.

라이브러리 선택

lodash를 쓴다면 lodash-es를 쓰는 게 좋다. ES 모듈로 제공되어서 Tree Shaking이 잘 된다.

// lodash - CommonJS, Tree Shaking 안됨
import { debounce } from 'lodash';

// lodash-es - ES 모듈, Tree Shaking 됨
import { debounce } from 'lodash-es';

확인 방법

번들 결과물을 분석해서 Tree Shaking이 제대로 됐는지 확인할 수 있다.

npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

빌드하면 번들 구성을 시각적으로 보여준다. 예상보다 큰 라이브러리가 있으면 Tree Shaking이 안 된 거다.

정리

  • Tree Shaking은 사용하지 않는 코드를 제거하는 최적화 기법
  • ES6 모듈을 써야 동작함
  • sideEffects: false 설정으로 더 효과적으로 동작
  • 라이브러리 선택 시 ES 모듈 버전이 있는지 확인