Tree shaking (트리쉐이킹)이란
트리쉐이킹이란 사용하지 않는 코드를 제거하는 최적화 기법이에요.
그럼 이 트리쉐이킹을 어떻게 할 수 있을까요?
번들러
현대적인 웹개발에선 webpack, Rollup, esbuild, Rspack과 같은 다양한 번들러를 사용해 여러 모듈화된 파일을 하나로 합하는 작업을 하게 되는데요.
이때 문제되는 것이 파일들을 통합할 때 불필요한 코드가 함께 포함될 수도 있어요. 이를 해결하기 위해 번들러는 위에서 설명한 트리쉐이킹을 진행하게 돼요.
그렇다면 번들러는 어떤 코드를 놔둬야하고 제거해야하는지 판별할 수 있을까요?
AST 분석
AST 분석이란 ES2015의 정적 모듈 가져오기 구문인 import
, export
를 통해 모듈의 필요성을 확인하는 작업이에요.
여기서 ES2015에서 도입된 정적 모듈 가져오기에 조금 집중해볼 필요가 있어요.
기존 CommonJS(CJS)에선 require
문을 통해 모듈을 가져오는데, 이때 모듈 의존성이 런타임에 결정될 수도 있어서 번들러는 트리쉐이킹이 어렵다고 판단하게 돼요.
ECMAScript modules(ESM)에선 아래 사진처럼 빌드타임에 의존성 분석이 가능하기에 트리쉐이킹에 용이해요.
sideEffects
sideEffects
는 package.json
에 명시하는 필드로, 패키지의 어떤 파일이 사이드 이펙트를 가지고 있는지 번들러에게 알려주는 역할을 해요.
여기서 사이드 이펙트란 모듈이 import될 때 전역 스코프에 영향을 미치는 코드를 의미해요. 예를 들어 전역 객체를 수정하거나, 프로토타입을 확장하는 등의 작업이죠.
{
"name": "some-packages",
"sideEffects": false
}
sideEffects: false
로 설정하면 해당 패키지의 모든 파일이 사이드 이펙트가 없다고 선언하는 거예요. 이는 번들러에게 "사용되지 않는 export는 안전하게 제거해도 된다"는 신호를 주게 되죠.
특정 파일만 사이드 이펙트가 있다고 표시할 수도 있어요:
{
"name": "your-package",
"sideEffects": [
".css",
"./src/some-side-effect.js"
]
}
이렇게 설정하면 CSS 파일들과 특정 자바스크립트 파일만 사이드 이펙트가 있다고 표시되어, 나머지 파일들은 더 효과적으로 트리쉐이킹될 수 있어요.
더 정확하겐 sideEffects에 해당하는 파일들을 번들러가 자동으로 제거하지 않도록 도와줘요.
라이브러리단에서의 고민
1. 선택적 기능 임포트
많은 모던 라이브러리들은 트리쉐이킹을 최적화하기 위해 "선택적 기능 임포트" 패턴을 채택하고 있어요. 예를 들어 TanStack Table의 경우:
// 기본 기능만 사용
import { useReactTable } from '@tanstack/react-table'
// 필요한 기능을 추가로 임포트
import {
getCoreRowModel,
getSortedRowModel,
getPaginationRowModel
} from '@tanstack/react-table'
이런 방식의 장점은:
- 사용자가 필요한 기능만 정확히 임포트할 수 있음
- 번들 사이즈를 효과적으로 제어할 수 있음
- 사용하지 않는 기능은 자동으로 트리쉐이킹됨
차트 라이브러리들도 이런 패턴을 따르고 있어요. Recharts의 경우를 살펴볼까요?
// ❌ 피해야 할 패턴 (불필요한 차트 컴포넌트까지 모두 포함)
import {
LineChart,
BarChart,
PieChart,
AreaChart,
// ... 다른 모든 차트 타입
} from 'recharts'
// ✅ 권장하는 패턴 (필요한 컴포넌트만 임포트)
import { LineChart, Line, XAxis, YAxis, Tooltip } from 'recharts'
// 실제 사용 예시
const MyChart = () => (
<LineChart width={400} height={400} data={data}>
<Line type="monotone" dataKey="value" stroke="#8884d8" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
</LineChart>
)
이런 방식의 장점은:
- 실제 사용하는 차트 타입과 컴포넌트만 번들에 포함
- 예를 들어
BarChart
,PieChart
등 사용하지 않는 차트는 최종 번들에서 제외 - 각 차트에서 사용하는 부가 기능(
Tooltip
,Legend
등)도 필요한 것만 선택적으로 포함
특히 Recharts는 각 컴포넌트가 독립적으로 설계되어 있어서:
- 필요한 컴포넌트만 정확하게 임포트 가능
- 내부적으로 의존성이 명확하게 분리되어 있음
- 트리쉐이킹이 매우 효과적으로 동작
2. Subpath Exports 활용
패키지의 특정 기능을 별도의 진입점으로 분리하는 방식도 있어요. package.json
에서 다음과 같이 설정할 수 있죠:
Subpath exports는 트리쉐이킹이 아닌 패키지 구조화를 위한 기능이에요.
다만 CommonJS 환경에서는 코드 분할 측면에서 도움이 될 수 있어요.
{
"name": "my-library",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
},
"./core": {
"import": "./dist/esm/core/index.js",
"require": "./dist/cjs/core/index.js"
},
"./utils": {
"import": "./dist/esm/utils/index.js",
"require": "./dist/cjs/utils/index.js"
}
}
}
이렇게 설정하면 ESM과 CommonJS 모두에서 각각의 방식으로 임포트할 수 있어요:
// ESM
import { core } from 'my-library/core'
// CommonJS
const { core } = require('my-library/core')
이 방식의 장점:
- ESM과 CommonJS 모두 지원 가능
- 더 세밀한 코드 분할이 가능
- 사용자가 필요한 기능만 정확히 선택 가능
- 번들러의 트리쉐이킹 효율이 향상됨
3. 부수 효과 최소화
라이브러리 설계 시 부수 효과를 최소화하는 것도 중요해요:
// ❌ 피해야 할 패턴
import { initializeLibrary } from 'my-library'
// 자동으로 전역 설정을 수정함
initializeLibrary()
// ✅ 권장하는 패턴
import { createLibrary } from 'my-library'
// 사용자가 명시적으로 설정을 제어
const instance = createLibrary({
// 설정 옵션
})
이렇게 하면:
- 사용자가 기능을 더 예측 가능하게 사용할 수 있음
- 트리쉐이킹이 더 효과적으로 동작
- 테스트와 디버깅이 용이
결론
트리쉐이킹을 효과적으로 적용하기 위해서는 다음과 같은 패턴을 따르는 것이 좋아요:
개발자가 할 수 있는 것들
-
ESM 사용하기
- CommonJS(
require
) 대신 ES Modules(import
/export
) 사용 - 동적 임포트는 필요한 경우에만 제한적으로 사용
- CommonJS(
-
선택적 임포트 활용
// ❌ 피하기 import * as utils from './utils' // ✅ 권장 import { specificUtil } from './utils'
-
사이드 이펙트 최소화
- 전역 객체 수정을 피하기
- 순수 함수 지향하기
package.json
에sideEffects
필드 명시
라이브러리 제작자가 할 수 있는 것들
-
모듈 구조 최적화
- 기능별로 모듈 분리
- Subpath exports 활용
- 독립적인 컴포넌트 설계
-
명시적인 API 설계
- 사용자가 필요한 기능만 선택할 수 있게 하기
- 암묵적인 의존성 피하기
이러한 패턴들을 잘 활용하면 번들 크기를 효과적으로 줄일 수 있고, 결과적으로 애플리케이션의 성능 향상을 기대할 수 있어요.