Tree Shaking

트리 쉐이킹은 현대 자바스크립트 애플리케이션의 번들 크기를 최적화하는 중요한 기술이다.

트리 쉐이킹은 현대 웹 애플리케이션 최적화의 필수 요소가 되었다.

최적의 결과를 얻기 위한 권장 사항은 다음과 같다:

  1. ES 모듈 사용: 모든 코드를 ESM 형식으로 작성하고 사용.
  2. 번들러 선택: Rollup 또는 Webpack과 같은 트리 쉐이킹을 지원하는 번들러를 사용.
  3. 명시적 가져오기: import * as보다 import { specificFunction }을 선호.
  4. 사이드 이펙트 관리: sideEffects 속성을 설정하고 사이드 이펙트를 최소화.
  5. 최신 라이브러리 선택: 트리 쉐이킹을 지원하는 라이브러리를 선택.
  6. 번들 분석: Bundle Analyzer를 사용하여 결과를 모니터링하고 개선.
  7. 순수 함수형 접근 방식: 가능한 한 순수 함수를 작성하고 사용.

자바스크립트 애플리케이션 최적화의 다른 측면과 마찬가지로, 트리 쉐이킹은 세심한 설계와 지속적인 개선이 필요한 분야이다. 적절하게 구현된다면 사용자 경험과 애플리케이션 성능에 상당한 개선을 가져올 수 있다.

트리 쉐이킹의 개념과 원리

정의

트리 쉐이킹(Tree Shaking)은 사용되지 않는 코드를 최종 번들에서 제거하는 프로세스이다.
이름은 나무를 흔들어 죽은 잎이나 가지를 떨어뜨리는 것에서 유래했다.
자바스크립트 컨텍스트에서는, 애플리케이션에서 실제로 사용되지 않는 코드(“데드 코드”)를 제거하는 것을 의미한다.

작동 원리

트리 쉐이킹의 기본 원리는 다음과 같다:

  1. 정적 분석: 코드를 실행하지 않고 구조를 분석한다.
  2. 의존성 그래프 구축: 모듈 간의 가져오기/내보내기 관계를 기반으로 의존성 그래프를 만든다.
  3. 사용되는 코드 식별: 애플리케이션의 진입점(entry point)에서 시작하여 실제로 접근되는 코드 경로를 추적한다.
  4. 미사용 코드 제거: 의존성 그래프에서 접근되지 않는 코드를 최종 번들에서 제외한다.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// utils.js
export function used() {
  return '이 함수는 사용됩니다';
}

export function unused() {
  return '이 함수는 사용되지 않습니다';
}

// main.js
import { used } from './utils.js';

console.log(used());  // used 함수만 번들에 포함됨

위 예제에서 unused 함수는 가져오지 않고 사용하지 않았기 때문에 최종 번들에서 제거된다.

트리 쉐이킹을 위한 필수 조건

트리 쉐이킹이 효과적으로 작동하기 위해서는 몇 가지 중요한 조건이 필요하다:

  1. ES 모듈(ESM) 사용
    트리 쉐이킹은 ES 모듈의 정적 구조에 크게 의존한다.
    ES 모듈은 정적 가져오기/내보내기 구문을 사용하므로 번들러가 빌드 시간에 의존성 그래프를 정확하게 분석할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    
    // ESM - 트리 쉐이킹 가능
    import { Component } from './components';
    export function helper() { /* … */ }
    
    // CommonJS - 트리 쉐이킹 어려움
    const Component = require('./components').Component;
    module.exports.helper = function() { /* … */ };
    
  2. 사이드 이펙트 최소화
    사이드 이펙트(부작용)가 있는 코드는 트리 쉐이킹을 방해할 수 있다. 사이드 이펙트란 모듈이 로드될 때 다른 모듈이나 전역 상태를 변경하는 것을 의미한다.

    1
    2
    3
    4
    
    // 사이드 이펙트 예시 - 트리 쉐이킹 방해
    export function helper() { /* … */ }
    console.log('이 모듈이 로드되었습니다'); // 사이드 이펙트
    document.title = '제목 변경';           // 사이드 이펙트
    
  3. 순수 함수 선호
    순수 함수(입력이 같으면 항상 같은 출력을 반환하는 함수)는 트리 쉐이킹에 이상적이다.
    외부 상태에 의존하거나 외부 상태를 변경하지 않기 때문이다.

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    
    // 순수 함수 - 트리 쉐이킹에 이상적
    export function add(a, b) {
      return a + b;
    }
    
    // 비순수 함수 - 트리 쉐이킹 어려움
    let counter = 0;
    export function increment() {
      counter++;
      return counter;
    }
    

트리 쉐이킹 최적화 전략

효과적인 트리 쉐이킹을 위한 몇 가지 주요 전략은 다음과 같다:

  1. 내보내기/가져오기 최적화

    1
    2
    3
    4
    5
    6
    7
    
    // 나쁜 예 - 전체 모듈 가져오기
    import _ from 'lodash';
    const arr = _.concat([1], [2]);
    
    // 좋은 예 - 필요한 함수만 가져오기
    import { concat } from 'lodash-es';
    const arr = concat([1], [2]);
    
  2. 사이드 이펙트 관리

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    
    // package.json
    {
      "name": "my-lib",
      // 옵션 1: 사이드 이펙트 없음
      "sideEffects": false,
    
      // 옵션 2: 특정 파일만 사이드 이펙트 있음
      "sideEffects": [
        "*.css",
        "src/polyfills.js"
      ]
    }
    
  3. 순수 함수 사용

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    
    // 나쁜 예 - 전역 상태 수정
    let globalCache = {};
    export function getData(key) {
      if (!globalCache[key]) {
        globalCache[key] = fetchData(key);
      }
      return globalCache[key];
    }
    
    // 좋은 예 - 순수 함수
    export function processData(data) {
      return data.map(item => item * 2);
    }
    
  4. 바벨 플러그인 활용
    바벨의 transform-imports 같은 플러그인을 사용해 자동으로 가져오기를 최적화할 수 있다:

    1
    2
    3
    4
    5
    6
    
    // 설정 전
    import { Button, Card } from 'material-ui';
    
    // 설정 후 자동 변환
    import Button from 'material-ui/Button';
    import Card from 'material-ui/Card';
    

트리 쉐이킹의 영향 및 성능 개선

트리 쉐이킹이 실제 애플리케이션에 미치는 영향을 살펴보면:

  1. 번들 크기 감소
    트리 쉐이킹을 통해 다음과 같은 번들 크기 감소를 기대할 수 있다:

    • 작은 라이브러리: 10-30% 감소
    • 대형 프레임워크: 40-60% 감소
    • 유틸리티 라이브러리: 최대 90% 감소(예: Lodash)
  2. 로딩 및 실행 성능 개선
    번들 크기 감소는 다음과 같은 성능 개선으로 이어진다:

    • 초기 로딩 시간 단축: 더 작은 번들로 다운로드 시간 단축
    • 파싱 시간 감소: 브라우저가 처리해야 할 코드 양 감소
    • 실행 시간 개선: 초기화해야 할 코드 감소

사례 연구: Lodash

Lodash는 트리 쉐이킹의 중요성을 보여주는 대표적인 예:

1
2
3
4
5
6
// 트리 쉐이킹 없이 전체 Lodash 가져오기 (~70KB 압축)
import _ from 'lodash';

// 트리 쉐이킹으로 필요한 함수만 가져오기 (~3KB 압축)
import map from 'lodash-es/map';
import filter from 'lodash-es/filter';

트리 쉐이킹의 한계와 도전 과제

트리 쉐이킹이 항상 완벽한 해결책은 아니다.
몇 가지 한계와 도전 과제가 있다:

  1. 동적 가져오기의 어려움

    1
    2
    3
    
    // 동적 가져오기 - 트리 쉐이킹 어려움
    const componentName = getComponentName();
    const Component = require(`./components/${componentName}`);
    
  2. CommonJS와의 호환성 문제

    1
    2
    3
    
    // CommonJS 모듈은 트리 쉐이킹하기 어려움
    const utils = require('./utils');
    utils.someFunction();
    
  3. 사이드 이펙트 감지의 어려움

    1
    2
    3
    4
    5
    6
    
    // 번들러가 사이드 이펙트를 정확히 감지하기 어려운 예
    export class MyComponent {
      constructor() {
        document.addEventListener('click', this.handleClick);
      }
    }
    
  4. 프로토타입 수정과 관련된 문제

    1
    2
    
    // 프로토타입 수정은 사이드 이펙트로 간주됨
    Array.prototype.customMethod = function() { /* … */ };
    

ESM vs. CommonJS 트리 쉐이킹 비교

모듈 시스템트리 쉐이킹 효과정적 분석 용이성번들러 지원추가 구성 필요
ES Modules (ESM)우수함 (90-100%)매우 높음모든 주요 번들러최소 또는 없음
CommonJS제한적 (0-40%)낮음일부 번들러만 부분 지원복잡한 구성 필요
AMD제한적 (20-50%)중간일부 번들러중간 수준
UMD제한적 (10-30%)낮음일부 번들러 제한적 지원복잡한 구성 필요

ESM 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// math.js (ESM)
export const add = (a, b) => a + b;
export const subtract = (a, b) => a - b;
export const multiply = (a, b) => a * b;
export const divide = (a, b) => a / b;

// main.js (ESM)
import { add, multiply } from './math.js';
console.log(add(2, 3));
console.log(multiply(4, 5));

결과: subtractdivide 함수는 최종 번들에서 제외된다.

CommonJS 예시

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// math.js (CommonJS)
exports.add = (a, b) => a + b;
exports.subtract = (a, b) => a - b;
exports.multiply = (a, b) => a * b;
exports.divide = (a, b) => a / b;

// main.js (CommonJS)
const { add, multiply } = require('./math.js');
console.log(add(2, 3));
console.log(multiply(4, 5));

결과: 일반적으로 모든 함수가 번들에 포함되지만, 일부 번들러는 특별한 분석을 통해 제한적인 트리 쉐이킹을 시도한다.

트리 쉐이킹에 관한 번들러, 프레임워크, 라이브러리 비교

주요 번들러의 트리 쉐이킹 구현

다양한 자바스크립트 번들러는 각기 다른 방식으로 트리 쉐이킹을 구현한다:

번들러트리 쉐이킹 기본 지원구성 복잡성ESM 지원CommonJS 지원사이드 이펙트 처리성능
Webpack지원 (v2+)중간완전 지원제한적 지원sideEffects 플래그 지원중간
Rollup처음부터 내장낮음완전 지원플러그인 필요세밀한 제어 가능우수함
ESBuild기본 내장매우 낮음완전 지원제한적 지원기본 지원매우 우수함
Parcel기본 내장거의 없음완전 지원제한적 지원기본 지원우수함
Browserify플러그인 필요높음플러그인 필요기본 지원제한적낮음
Webpack

Webpack은 가장 널리 사용되는 번들러 중 하나로, 트리 쉐이킹을 위해 다음과 같은 기능을 제공한다:

  • 프로덕션 모드: mode: 'production' 설정으로 자동 최적화
  • 사이드 이펙트 표시: package.jsonsideEffects 필드 활용
  • Terser 통합: 미사용 코드 제거를 위한 최종 최적화
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,  // 트리 쉐이킹 활성화
    minimize: true      // 미니파이어 사용
  }
};

// package.json
{
  "name": "my-package",
  "sideEffects": false  // 사이드 이펙트 없음을 표시
}
Rollup

Rollup은 트리 쉐이킹에 초점을 맞춘 번들러로, 기본적으로 더 효과적인 트리 쉐이킹을 제공한다:

  • ES 모듈 중심: 기본적으로 ES 모듈에 최적화됨
  • 정교한 정적 분석: 더 철저한 데드 코드 검출
  • 코드 분할 및 동적 가져오기 지원
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// rollup.config.js
export default {
  input: 'src/main.js',
  output: {
    file: 'bundle.js',
    format: 'esm'
  },
  treeshake: {
    moduleSideEffects: false,   // 모듈 사이드 이펙트 없음
    propertyReadSideEffects: false  // 속성 읽기에 사이드 이펙트 없음
  }
};
ESBuild

ESBuild는 속도에 최적화된 최신 번들러로 트리 쉐이킹도 지원한다:

  • Go로 작성: 매우 빠른 성능
  • 기본 내장 트리 쉐이킹
  • 사이드 이펙트 표시 지원
1
2
3
4
5
6
7
8
// esbuild.js
require('esbuild').build({
  entryPoints: ['src/main.js'],
  bundle: true,
  minify: true,
  treeShaking: true,  // 명시적 활성화(기본값은 true)
  outfile: 'dist/bundle.js'
});

다양한 프레임워크에서의 트리 쉐이킹

프레임워크트리 쉐이킹 지원기본 모듈 시스템사이드 이펙트 관리코드 분할 지원
React매우 좋음ESM컴포넌트 기반 분리 가능React.lazy()
Vue.js좋음ESM컴포저블 API로 개선됨비동기 컴포넌트
Angular매우 좋음ESMNgModules 시스템지연 로딩 모듈
Svelte우수함ESM컴파일 타임 최적화동적 가져오기
Alpine.js중간ESM/IIFE제한적제한적
React

React 애플리케이션에서 트리 쉐이킹 최적화:

  • 컴포넌트 단위 분리: 각 컴포넌트를 개별 파일로 관리
  • React.lazy()와 동적 가져오기: 필요할 때만 컴포넌트 로드
  • HOC 및 렌더 프롭스 최적화: 불필요한 래퍼 컴포넌트 제거
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 동적 컴포넌트 로딩으로 트리 쉐이킹 개선
const AdminPanel = React.lazy(() => import('./AdminPanel'));

function App() {
  return (
    <Suspense fallback={<Loading />}>
      {isAdmin && <AdminPanel />}
    </Suspense>
  );
}
Vue.js

Vue.js에서 트리 쉐이킹 최적화:

  • ES 모듈 빌드 사용: import Vue from 'vue/dist/vue.esm.js'
  • 컴포넌트 로컬 등록: 전역 등록 대신 로컬 등록 활용
  • 비동기 컴포넌트: 필요할 때만 로드
1
2
3
4
// Vue 3의 컴포저블 API로 트리 쉐이킹 개선
import { ref, computed, onMounted } from 'vue';

// 필요한 API만 번들에 포함됨
Angular

Angular에서 트리 쉐이킹 최적화:

  • Angular CLI 빌드 최적화: 기본 지원됨
  • 지연 로딩 모듈: 필요할 때만 모듈 로드
  • NgModules 분리: 기능별로 모듈 분리
1
2
3
4
5
6
7
// 라우팅에서 지연 로딩으로 트리 쉐이킹 개선
const routes: Routes = [
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule)
  }
];

트리 쉐이킹 지원 라이브러리

트리 쉐이킹에 최적화된 몇 가지 주요 라이브러리:

라이브러리트리 쉐이킹 지원ESM 버전 제공권장 가져오기 방법번들 감소 효과
Lodash기본 버전은 제한적lodash-es개별 함수 가져오기85-95%
Moment.js미지원아니오대안 사용 권장0%
date-fns완전 지원필요한 함수만 가져오기90-98%
Material-UI완전 지원경로 가져오기60-80%
RxJSv6+ 지원파이프 가능 연산자 사용70-90%
jQuery미지원아니오대안 사용 권장0%
Ramda지원개별 함수 가져오기85-95%
Lodash-es

Lodash의 ES 모듈 버전:

1
2
// 트리 쉐이킹 지원
import { map, filter } from 'lodash-es';
Date-fns

날짜 처리를 위한 함수형 유틸리티 라이브러리:

1
2
// 트리 쉐이킹 지원
import { format, addDays } from 'date-fns';
Material-UI / MUI

React UI 라이브러리:

1
2
3
// 트리 쉐이킹 지원
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';

실제 프로젝트에서의 트리 쉐이킹 전후 비교

실제 프로젝트에서 트리 쉐이킹 적용 전후 비교:

전형적인 프로젝트 예시 (가상 데이터)

최적화 단계번들 크기로딩 시간실행 시간
최적화 전1.2MB780ms350ms
트리 쉐이킹 적용720KB480ms280ms
추가 최적화520KB320ms230ms

Moment.js vs. Date-fns

Moment.js(트리 쉐이킹 미지원)에서 date-fns(트리 쉐이킹 지원)로 마이그레이션:

라이브러리가져오기 방식번들 크기
Moment.jsimport moment from 'moment'~230KB
date-fnsimport { format } from 'date-fns'~3KB

트리 쉐이킹 진단 및 분석 도구

트리 쉐이킹 효과를 분석하고 개선하기 위한 도구들이 있다:

Webpack Bundle Analyzer

번들 내용과 크기를 시각화하여 개선 지점 식별:

1
npm install --save-dev webpack-bundle-analyzer
1
2
3
4
5
6
7
8
// webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

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

Rollup Plugin Visualizer

Rollup 번들을 시각화:

1
npm install --save-dev rollup-plugin-visualizer
1
2
3
4
5
6
7
8
// rollup.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default {
  plugins: [
    visualizer()
  ]
};

Source-map-explorer

소스맵을 사용하여 번들 구성 분석:

1
npm install --save-dev source-map-explorer
1
2
3
4
5
6
// package.json
{
  "scripts": {
    "analyze": "source-map-explorer 'build/static/js/*.js'"
  }
}

참고 및 출처