June 28, 2025

거대한 라이브러리 통째로 가져오기: 트리 쉐이킹의 저주 🌳📦

JavaScript
빌드&번들링
성능
UX
아키텍처

Summary

불필요하게 거대한 라이브러리 전체를 임포트하면 번들 크기가 커지고 웹사이트 로딩 속도가 느려져 사용자 경험에 악영향을 줍니다. 필요한 모듈만 개별적으로 가져오거나, 트리 쉐이킹이 잘 적용되도록 빌드 설정을 최적화하고 트리 쉐이킹 친화적인 라이브러리를 선택하여 성능을 개선해야 합니다.

Why Wrong?

모던 JavaScript 프로젝트에서 import * as LibraryName from 'library-name'; 또는 import 'library-name';와 같이 라이브러리 전체를 가져오는 것은 심각한 번들 크기 증가를 초래합니다. 대부분의 경우, 라이브러리 내 모든 기능을 사용하지 않음에도 불구하고, 사용되지 않는 코드(dead code)까지 최종 번들에 포함되어 사용자에게 전달됩니다. 이는 다음과 같은 문제점을 야기합니다:

  1. 번들 크기 증가: 웹페이지 로딩 시간이 길어지고, 특히 모바일 환경이나 저속 네트워크 사용자에게 치명적입니다.
  2. 네트워크 대역폭 낭비: 사용자가 필요 없는 코드까지 다운로드하게 되어 데이터 요금을 불필요하게 소모합니다.
  3. 파싱 및 실행 시간 증가: 브라우저가 더 많은 JavaScript 코드를 파싱하고 실행해야 하므로, First Contentful Paint (FCP) 및 Time To Interactive (TTI) 지연으로 이어집니다.
  4. 캐싱 효율 저하: 번들 파일이 커지면 캐싱의 이점이 줄어들 수 있습니다.

이러한 현상은 주로 모듈 번들러(Webpack, Rollup, Parcel 등)의 '트리 쉐이킹(Tree Shaking)' 기능이 제대로 작동하지 않거나, 라이브러리가 트리 쉐이킹 친화적으로 설계되지 않았을 때 발생합니다. import * 문법은 번들러가 어떤 함수나 객체가 실제로 사용되는지 파악하기 어렵게 만들 수 있으며, 사이드 이펙트가 있는 모듈 전체를 임포트할 때도 유사한 문제가 발생합니다.

How to Fix?

번들 크기를 최적화하고 애플리케이션의 성능을 향상시키기 위해 다음 전략들을 활용해야 합니다:

  1. 모듈별로 필요한 기능만 임포트: 대부분의 모던 라이브러리는 특정 기능만 가져올 수 있도록 모듈 방식을 지원합니다. 예를 들어, lodashdebounce 함수만 필요하다면 import debounce from 'lodash/debounce';와 같이 개별적으로 가져올 수 있습니다. 이는 lodash가 서브 모듈을 제공하기 때문이며, lodash-es와 같이 ES 모듈을 지원하는 버전을 사용하면 더욱 효과적인 트리 쉐이킹이 가능합니다.
  2. 이름이 지정된 임포트(Named Imports) 활용: import { specificFunction } from 'library-name';와 같이 명시적으로 필요한 기능만 가져오면 번들러가 사용되지 않는 코드를 쉽게 제거(트리 쉐이킹)할 수 있습니다.
  3. 트리 쉐이킹 친화적인 라이브러리 선택: 라이브러리가 package.jsonsideEffects: false 또는 module 필드를 명시하여 ES 모듈을 제공하는지 확인하세요. 이는 번들러가 더욱 효과적으로 트리 쉐이킹을 수행할 수 있도록 돕습니다.
  4. 빌드 도구 설정 최적화: Webpack의 optimization.usedExports, Rollup의 treeshake 옵션 등을 통해 트리 쉐이킹이 올바르게 동작하도록 설정해야 합니다. 프로덕션 빌드 시에는 UglifyJS, Terser 등의 플러그인을 사용하여 사용되지 않는 코드를 제거하고 난독화하는 것도 중요합니다.
  5. 번들 분석 도구 사용: 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();