June 28, 2025
거대한 라이브러리 통째로 가져오기: 트리 쉐이킹의 저주 🌳📦
JavaScript
빌드&번들링
성능
UX
아키텍처
Summary
불필요하게 거대한 라이브러리 전체를 임포트하면 번들 크기가 커지고 웹사이트 로딩 속도가 느려져 사용자 경험에 악영향을 줍니다. 필요한 모듈만 개별적으로 가져오거나, 트리 쉐이킹이 잘 적용되도록 빌드 설정을 최적화하고 트리 쉐이킹 친화적인 라이브러리를 선택하여 성능을 개선해야 합니다.
Why Wrong?
모던 JavaScript 프로젝트에서 import * as LibraryName from 'library-name';
또는 import 'library-name';
와 같이 라이브러리 전체를 가져오는 것은 심각한 번들 크기 증가를 초래합니다. 대부분의 경우, 라이브러리 내 모든 기능을 사용하지 않음에도 불구하고, 사용되지 않는 코드(dead code)까지 최종 번들에 포함되어 사용자에게 전달됩니다. 이는 다음과 같은 문제점을 야기합니다:
- 번들 크기 증가: 웹페이지 로딩 시간이 길어지고, 특히 모바일 환경이나 저속 네트워크 사용자에게 치명적입니다.
- 네트워크 대역폭 낭비: 사용자가 필요 없는 코드까지 다운로드하게 되어 데이터 요금을 불필요하게 소모합니다.
- 파싱 및 실행 시간 증가: 브라우저가 더 많은 JavaScript 코드를 파싱하고 실행해야 하므로, First Contentful Paint (FCP) 및 Time To Interactive (TTI) 지연으로 이어집니다.
- 캐싱 효율 저하: 번들 파일이 커지면 캐싱의 이점이 줄어들 수 있습니다.
이러한 현상은 주로 모듈 번들러(Webpack, Rollup, Parcel 등)의 '트리 쉐이킹(Tree Shaking)' 기능이 제대로 작동하지 않거나, 라이브러리가 트리 쉐이킹 친화적으로 설계되지 않았을 때 발생합니다. import *
문법은 번들러가 어떤 함수나 객체가 실제로 사용되는지 파악하기 어렵게 만들 수 있으며, 사이드 이펙트가 있는 모듈 전체를 임포트할 때도 유사한 문제가 발생합니다.
How to Fix?
번들 크기를 최적화하고 애플리케이션의 성능을 향상시키기 위해 다음 전략들을 활용해야 합니다:
- 모듈별로 필요한 기능만 임포트: 대부분의 모던 라이브러리는 특정 기능만 가져올 수 있도록 모듈 방식을 지원합니다. 예를 들어,
lodash
의debounce
함수만 필요하다면import debounce from 'lodash/debounce';
와 같이 개별적으로 가져올 수 있습니다. 이는lodash
가 서브 모듈을 제공하기 때문이며,lodash-es
와 같이 ES 모듈을 지원하는 버전을 사용하면 더욱 효과적인 트리 쉐이킹이 가능합니다. - 이름이 지정된 임포트(Named Imports) 활용:
import { specificFunction } from 'library-name';
와 같이 명시적으로 필요한 기능만 가져오면 번들러가 사용되지 않는 코드를 쉽게 제거(트리 쉐이킹)할 수 있습니다. - 트리 쉐이킹 친화적인 라이브러리 선택: 라이브러리가
package.json
에sideEffects: false
또는module
필드를 명시하여 ES 모듈을 제공하는지 확인하세요. 이는 번들러가 더욱 효과적으로 트리 쉐이킹을 수행할 수 있도록 돕습니다. - 빌드 도구 설정 최적화: Webpack의
optimization.usedExports
, Rollup의treeshake
옵션 등을 통해 트리 쉐이킹이 올바르게 동작하도록 설정해야 합니다. 프로덕션 빌드 시에는 UglifyJS, Terser 등의 플러그인을 사용하여 사용되지 않는 코드를 제거하고 난독화하는 것도 중요합니다. - 번들 분석 도구 사용:
webpack-bundle-analyzer
와 같은 도구를 사용하여 어떤 모듈이 번들에 가장 많은 공간을 차지하는지 시각적으로 확인하고 최적화 대상을 식별하세요.
Before Code (Bad)
// lodash 전체를 임포트하는 경우
import _ from 'lodash';
function processData(data) {
// 필요한 것은 debounce 함수 뿐이지만, lodash 전체가 번들에 포함됩니다.
const debouncedFunc = _.debounce(() => {
console.log('Debounced:', data);
}, 300);
debouncedFunc();
}
processData([1, 2, 3]);
// moment.js와 같이 거대한 라이브러리 전체를 임포트하는 경우
import moment from 'moment';
function displayTime() {
// 날짜 포맷팅 하나만 필요하지만, 모든 locale 데이터와 기능이 번들에 포함됩니다.
console.log(moment().format('YYYY-MM-DD HH:mm:ss'));
}
displayTime();
// 모든 아이콘을 임포트하는 경우 (react-icons 예시)
import { FaBeer, FaCat, FaDog, FaAmbulance, FaAnchor /* ... 수백 개 */ } from 'react-icons/fa';
function MyComponent() {
// 실제로는 FaBeer 하나만 사용하는데, 수백 개의 아이콘이 모두 번들에 포함될 수 있습니다.
return <FaBeer />;
}
MyComponent();
After Code (Good)
// lodash에서 필요한 debounce 함수만 임포트하는 경우
// lodash-es를 사용하거나, lodash의 특정 모듈 경로를 직접 지정
import debounce from 'lodash/debounce'; // 또는 'lodash-es'에서 import
function processData(data) {
const debouncedFunc = debounce(() => {
console.log('Debounced:', data);
}, 300);
debouncedFunc();
}
processData([1, 2, 3]);
// moment.js 대신 가볍고 트리 쉐이킹이 잘 되는 dayjs 사용
import dayjs from 'dayjs';
import 'dayjs/locale/ko'; // 필요한 locale만 임포트하여 불필요한 번들 증가 방지
dayjs.locale('ko');
function displayTime() {
console.log(dayjs().format('YYYY-MM-DD HH:mm:ss'));
}
displayTime();
// 필요한 아이콘만 임포트하는 경우
import { FaBeer } from 'react-icons/fa';
function MyComponent() {
return <FaBeer />;
}
MyComponent();