Skip to main content

Command Palette

Search for a command to run...

번들러가 코드 크기를 줄이는 진정한 방법 — Tree Shaking의 원리와 한계

Published
5 min read

프론트엔드 프로젝트가 대형화될수록 빌드 결과물의 크기가 점차 증가하는 것은 자연스러운 현상입니다.
기능의 확장과 라이브러리의 누적 사용으로 인해 번들 사이즈(Bundle Size) 가 커지게 되며, 이는 로딩 속도 및 사용자 경험 저하로 직결됩니다.

그러나 실제로 애플리케이션이 사용하는 코드는 전체 의존성의 일부에 불과합니다.
이때 사용되지 않는 코드를 제거하여 번들을 경량화하는 과정을 수행하는 기술이 바로 Tree Shaking입니다.
현대의 번들러(Webpack, Vite, Rollup 등)는 이 기능을 통해 불필요한 코드를 제거하고 최적화된 빌드 결과를 생성합니다.


🧠 Tree Shaking의 개념

Tree Shaking은 “사용되지 않는 코드(Dead Code)를 탐지하여 빌드 결과에서 제거하는 최적화 기법”을 의미합니다.
말 그대로 “트리의 불필요한 가지를 흔들어 떨어뜨리는 과정”에 비유할 수 있습니다.

모듈 그래프를 트리로 가정하면, 엔트리 포인트(루트)에서 실제로 참조되는 함수·변수·클래스만 남기고,
그 외로 연결되지 않은 export는 모두 제거됩니다.

예시

// utils.js
export function usedFn() {
  console.log("I'm used");
}
export function unusedFn() {
  console.log("I'm never used");
}

// main.js
import { usedFn } from './utils.js';
usedFn();

빌드 결과:

function usedFn() {
  console.log("I'm used");
}
usedFn();

unusedFn()은 import되지 않았기 때문에 최종 번들에서 제거됩니다.
이와 같이, Tree Shaking은 코드 의존성을 정적으로 분석하여 사용되지 않는 export를 안전하게 제거합니다.


⚙️ Tree Shaking의 전제 조건

Tree Shaking은 정적 분석(Static Analysis) 을 기반으로 작동하므로,
“어떤 코드가 사용되는가”를 빌드 시점에 정확히 판단할 수 있어야 합니다.
이때 가장 중요한 전제는 모듈 시스템의 형태입니다.
즉, 코드가 ES Module(import/export) 인지, CommonJS(require/module.exports) 인지에 따라 Tree Shaking의 가능 여부가 달라집니다.


🧩 ES Module (ESM) — 정적 분석이 가능한 모듈 시스템

ESM은 ECMAScript(ES6, 2015)에서 표준으로 도입된 정적 모듈 시스템입니다.
모듈 간 의존 관계가 코드 실행 이전에 확정되므로, 번들러가 전체 import/export 구조를 파악할 수 있습니다.

// math.js
export function add(a, b) { return a + b; }
export function sub(a, b) { return a - b; }

// main.js
import { add } from './math.js';
console.log(add(1, 2));

위 예시에서 번들러는 add만 사용되고 sub는 사용되지 않는다는 사실을 컴파일 타임에 알 수 있습니다.
따라서 sub()는 안전하게 제거되어 번들이 경량화됩니다.

항목설명
로딩 시점정적(빌드 시점)
불러오기 문법import ... from
내보내기 문법export, export default
Tree Shaking 가능 여부✅ 가능
주요 환경브라우저, Vite, Rollup, Webpack(ESM 모드)

🧱 CommonJS — 런타임 기반의 동적 모듈 시스템

CommonJS는 Node.js에서 채택된 런타임 모듈 시스템으로,
모듈의 구조와 export 내용이 실행 시점에 결정됩니다.
이는 번들러가 정적으로 분석할 수 없다는 의미이기도 합니다.

// math.js
function add(a, b) { return a + b; }
function sub(a, b) { return a - b; }
module.exports = { add, sub };

// main.js
const math = require('./math');
console.log(math.add(1, 2));

이 구조에서는 번들러가 sub()이 실제로 사용되지 않는다고 판단할 수 없습니다.
math 객체의 구성은 런타임에 결정되므로 Tree Shaking이 불가능합니다.

항목설명
로딩 시점런타임(동적)
불러오기 문법require()
내보내기 문법module.exports
Tree Shaking 가능 여부❌ 불가능
주요 환경Node.js (서버 환경 중심)

🧭 Tree Shaking이 ES Module 기반이어야 하는 이유

Tree Shaking은 정적 의존성 그래프를 분석하여 불필요한 export를 제거하는 방식으로 동작합니다.
따라서 모듈 간 관계를 빌드 시점에 확정할 수 있는 ESM(ES Module) 환경에서만 작동합니다.

// ✅ ESM (가능)
import { add } from './math.js'; // add만 사용 → sub 제거 가능

// ❌ CJS (불가능)
const math = require('./math');  // 런타임에서 math의 구조를 알 수 없음

Rollup과 Vite는 ESM 전용 번들러이며, Webpack 역시 ESM을 사용할 때만 Tree Shaking 최적화를 수행합니다.


📦 package.json의 "sideEffects" 속성 — Tree Shaking의 보호 장치

Tree Shaking은 미사용 export를 제거하지만,
일부 파일은 export가 없더라도 import 자체로 실행 효과(부수효과) 를 발생시킵니다.
대표적인 예시가 .css 파일의 import입니다.


🎨 .css 파일 import의 부수효과 (Side Effect)

// main.js
import './styles.css';

이 구문은 자바스크립트 실행 시점에 CSS를 브라우저에 주입하라는 의미입니다.
즉, .css 파일은 export를 제공하지 않더라도 DOM에 스타일을 적용하는 부수효과(Side Effect) 를 가집니다.

문제는 번들러 입장에서 .css 파일은 어떠한 변수를 export하지 않기 때문에
정적 분석 시 “사용되지 않는 코드(dead code)” 로 오인될 수 있다는 점입니다.

이를 방지하기 위해 package.json에 다음과 같이 설정합니다.

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

이 설정은 “CSS 파일은 단순 import만으로도 실행 효과가 있으므로 제거하지 말라”는 의미를 갖습니다.
해당 설정이 없을 경우, Tree Shaking 과정에서 CSS가 제거되어 UI가 정상적으로 표시되지 않는 문제가 발생할 수 있습니다.


🔍 Tree Shaking이 작동하지 않는 일반적인 사례

원인설명
CommonJS 의존성require() 기반 구조 → 정적 분석 불가
Babel 설정 문제preset-env에서 modules: commonjs로 변환 시 ESM 손실
sideEffects 누락CSS 또는 전역 실행 스크립트가 제거됨
동적 import 사용require(variable) 등 런타임 import는 분석 불가
라이브러리 자체의 ESM 미지원Rollup·Webpack이 dead code를 판별할 수 없음

⚡ 번들러별 Tree Shaking 적용 방식

번들러작동 방식특징
Webpackoptimization.usedExports"sideEffects" 설정을 조합앱 빌드 중심, 플러그인 제어 강력
Vite내부적으로 Rollup을 사용개발 시 HMR, 빌드 시 Rollup 기반 최적화
Rollup완전한 ESM 정적 분석라이브러리 번들용으로 가장 깨끗한 결과 제공

💡 결론 — 코드가 가벼워지는 이유는 “분석 가능한 구조”에 있다

Tree Shaking은 단순한 번들러의 기능이 아니라,
코드가 정적으로 분석 가능한 구조로 작성되어 있는가에 따라 성패가 갈립니다.

이를 위해 다음 세 가지를 실천하는 것이 중요합니다.

  1. ESM(ES Module) 문법을 사용하여 의존성을 정적으로 선언할 것

  2. 부수효과(Side Effect) 가 있는 파일(.css, 전역 초기화 스크립트 등)은 sideEffects에 명시할 것

  3. CommonJS 기반 라이브러리는 ESM 대체 버전(lodash-es, dayjs 등)으로 교체할 것

이러한 구조적 기반이 마련되어야 번들러가 불필요한 코드를 안전하게 제거할 수 있습니다.


정리
Tree Shaking은 번들러의 기술적 마법이 아니라, 모듈 시스템의 설계가 만들어낸 결과입니다.
ES Module을 기반으로 부수효과를 명확히 정의하는 것이
코드 크기 최적화의 근본적인 출발점입니다


🧾 작성 참고

이 글은 ChatGPT의 도움을 받아 내용을 정리하였습니다.

More from this blog

LangGraph의 astream_events: 에이전트 실행을 실시간으로 들여다보기

💡 한 줄 요약: astream_events()는 LangGraph 그래프 실행 중 발생하는 모든 이벤트(LLM 토큰, 툴 호출, 노드 시작/종료 등)를 실시간 스트림으로 받을 수 있는 비동기 API입니다. 들어가며 AI 에이전트가 응답을 생성하는 데 25초가 걸린다고 상상해보세요. 사용자는 평균 8초 후에 페이지를 이탈합니다. 하지만 스트리밍을 도입

Jun 20, 20266 min read

LLM은 어떻게 글을 읽고 쓰는가 — 입력부터 출력까지

대화형 AI에게 질문을 던지면, 마치 사람처럼 문장을 이해하고 답을 써 내려가는 것처럼 보인다.하지만 그 안에서 벌어지는 일은 생각보다 단순하고, 또 생각보다 기계적이다.이 글에서는 두 가지 질문에 답해 본다.LLM은 어떻게 글을 만들어내는가,그리고 우리가 입력한 프롬프트는 모델에 어떤 모습으로 들어가는가. 1부. LLM은 어떻게 글을 만들어내는가 핵심은

Jun 10, 20264 min read

useEffect 지옥이란 무엇이며, 어떻게 안전하게 사용하는가

React를 어느 정도 사용하다 보면 한 번쯤은“useEffect 지옥에 빠졌다”는 말을 듣거나 직접 느껴본 적이 있을 것이다. useEffect가 계속 늘어나고 의존성 배열은 점점 길어지고 왜 실행되는지 이해하기 어려워지며 eslint-disable-next-line 이 늘어나는 상태 하지만 많은 사람들이 오해한다.useEffect 지옥은 useEffect가 많아서 생기는 문제가 아니다. 이 글에서는 ‘useEffect 지옥’의 ...

Jan 12, 20263 min read

Zod란 무엇인가: TypeScript에서 런타임 검증과 타입 안정성을 동시에 해결하는 방법

TypeScript 프로젝트를 하다 보면 반드시 마주치는 문제가 있다.바로 “타입은 있는데, 데이터는 믿을 수 없다”는 것이다. TypeScript는 컴파일 타임에는 강력하지만,런타임에 들어오는 데이터에 대해서는 아무런 보장을 해주지 않는다. API 요청 데이터 사용자 입력(Form) 환경 변수 (process.env) 외부 JSON / 설정 파일 이 모든 것은 타입 시스템 바깥에서 들어온다. 이 문제를 해결하기 위해 등장한 라이브러...

Jan 10, 20263 min read

Flatpak을 이해하기 위한 배경 지식

배포판, 라이브러리 충돌, 그리고 OSTree까지 리눅스 데스크톱에서 Flatpak이 표준처럼 자리 잡은 데에는 분명한 이유가 있다.그 이유를 이해하려면 단순히 “Flatpak은 패키징 시스템이다”를 넘어서,리눅스 배포판 구조, 라이브러리 버전 충돌, OSTree라는 기술까지 함께 이해해야 한다. 이 글에서는 다음 질문에 답해본다. 리눅스에서 말하는 배포판이란 무엇인가? 라이브러리 버전 충돌은 왜 발생하는가? Flatpak은 이 문제를 어...

Jan 8, 20265 min read

Dev note

32 posts