Language/JS(Node.js)

트리 쉐이킹(Tree Shaking)의 내부 동작 원리: 번들러는 어떻게 죽은 코드를 찾아낼까?

Joonfluence 2026. 3. 21.

현대 자바스크립트 생태계에서 '트리 쉐이킹(Tree Shaking)'은 선택이 아닌 필수가 되었습니다. 프론트엔드의 로딩 속도 최적화는 물론이고, 백엔드의 서버리스(Serverless) 콜드 스타트 시간을 단축하기 위해서도 이 기술은 핵심적인 역할을 합니다.

그렇다면 Webpack, Rollup, esbuild 같은 번들러들은 도대체 무슨 수로 우리가 작성한 수많은 파일 속에서 '쓰이는 코드''버려야 할 코드'를 정확히 구분해 내는 걸까요? 그 내부 동작 원리를 깊이 있게 알아봅니다.


1. 전제 조건: 왜 반드시 ESM(ES Modules)이어야 할까?

트리 쉐이킹의 원리를 이해하기 위한 첫 번째 열쇠는 정적 분석(Static Analysis)입니다.

과거의 CommonJS(require)는 코드가 실행되는 시점(Runtime)에 모듈을 동적으로 가져왔습니다. 변수 값에 따라 가져올 모듈이 달라질 수도 있었죠. 번들러 입장에서는 코드를 직접 실행해 보기 전까지는 확신을 가질 수 없으니, 안전을 위해 모든 코드를 번들에 포함시켜야만 했습니다.

반면, ESM(import/export)은 파일의 최상단에서만 모듈을 선언할 수 있는 정적인 구조를 강제합니다. 덕분에 번들러는 코드를 실행하지 않고, 단순히 텍스트를 읽는(Parsing) 것만으로도 모듈 간의 의존성 지도를 완벽하게 그려낼 수 있습니다.


2. 딥 다이브: 트리 쉐이킹의 3단계 프로세스

트리 쉐이킹은 단순히 Ctrl+F로 텍스트를 찾는 수준이 아닙니다. 컴파일러 이론에 기반한 3단계 과정을 거칩니다.

Step 1: AST (Abstract Syntax Tree, 추상 구문 트리) 생성

번들러는 우리가 작성한 자바스크립트 코드를 문법적으로 잘게 쪼개어 AST(추상 구문 트리)라는 트리 형태의 데이터 구조로 변환합니다.
이 과정을 통해 코드는 단순한 문자열이 아니라, VariableDeclaration(변수 선언), FunctionDeclaration(함수 선언), ImportDeclaration(모듈 가져오기) 등 의미를 가진 노드(Node)들의 집합이 됩니다.

Step 2: 의존성 그래프(Dependency Graph) 구축 및 '마킹(Marking)'

AST가 만들어지면, 번들러는 진입점(Entry point, 보통 index.tsmain.js)부터 시작해서 import 구문을 따라가며 거대한 의존성 그래프를 그립니다.

이때 아주 중요한 '마킹(Marking)' 작업이 일어납니다.

  1. 진입점에서 실제로 호출된 함수나 변수에 "사용됨(Used)" 꼬리표를 붙입니다.
  2. 그 함수가 내부적으로 사용하는 다른 함수들에도 꼬리표를 전달합니다.
  3. 이 그래프 연결에서 소외된, 즉 아무도 호출하지 않은 export된 함수들은 꼬리표를 받지 못합니다.

Step 3: 스위핑 (Sweeping / Code Generation)

이제 번들러는 마킹이 끝난 AST를 바탕으로 최종 자바스크립트 파일을 다시 생성합니다. 이때 "사용됨" 꼬리표가 없는 노드(죽은 코드, Dead Code)들은 코드 생성 과정에서 완전히 누락시킵니다.
이것이 바로 나무를 흔들어 죽은 잎사귀를 떨어뜨리는 '트리 쉐이킹'의 실체입니다.


3. 트리 쉐이킹의 최대 적: '부수 효과 (Side Effects)'

이론적으로 완벽해 보이는 트리 쉐이킹에도 치명적인 약점이 있습니다. 바로 부수 효과(Side Effects)입니다.

번들러는 기본적으로 매우 보수적입니다. *"이 코드를 지웠을 때, 앱의 다른 동작에 영향을 주면 어떡하지?"*라는 의심을 항상 품고 있습니다.

// math.js
export const add = (a, b) => a + b;

// 🚨 문제가 되는 부수 효과 코드!
// 이 코드는 함수 외부의 전역 객체(window 또는 global)를 수정하고 있습니다.
window.MATH_VERSION = "1.0.0"; 

export const subtract = (a, b) => a - b;

만약 우리가 add 함수만 import 했다고 가정해 봅시다. 번들러는 subtract 함수는 지우겠지만, window.MATH_VERSION = "1.0.0"; 코드는 "이걸 지우면 프로그램 동작이 망가질 수도 있겠다!"라고 판단하여 지우지 못하고 번들에 포함시킵니다.

해결책: package.json"sideEffects": false

이러한 문제를 해결하기 위해, 라이브러리 제작자나 프로젝트 설정에서 "sideEffects": false라는 옵션을 명시합니다. 이는 번들러에게 "이 모듈 안에는 전역 상태를 오염시키는 부수 효과가 전혀 없으니, 안 쓰는 export는 안심하고 100% 다 날려버려도 돼!"라고 보증을 서주는 것과 같습니다.


4. 요약

  1. 트리 쉐이킹은 코드를 실행하지 않고 구조를 분석하는 ESM의 정적 특성 덕분에 가능합니다.
  2. 번들러는 코드를 AST로 변환하고, 의존성 그래프를 따라 사용된 코드만 마킹(Marking)합니다.
  3. 마킹되지 않은 코드는 최종 빌드에서 제거(Sweeping)됩니다.
  4. 완벽한 트리 쉐이킹을 위해서는 전역 상태를 건드리지 않는 순수 함수 위주의 작성과 부수 효과(Side Effects) 관리가 필수적입니다.
반응형

댓글