본문 바로가기
4주차

번들링, Tree Shaking과 Enum

by jstar00 2025. 5. 9.

안녕하세요 웹 임지성입니다.

이번 성능 최적화 스터디에서 번들링과 Tree Shaking에 대한 내용이 잠깐 등장했는데, 세미나 Typescript 강의자료에도 Enum에 대한 소개와 함께 트리쉐이킹 얘기가 나오더라고요.

이번 아티클에서는 두 내용을 연결지어, 번들링과 Tree Shaking, Enum과 성능 최적화에 대해 알아보겠습니다!


1. 번들링이 뭐야?

 

- 번들 정적 파일

번들: 웹 브라우저에 제공되도록 최적화된 정적 파일

정적 파일: 서버에서 특별한 처리 없이 그대로 클라이언트에게 전달되는 파일, 즉 브라우저가 바로 해석 가능한 파일

 

- 번들링

‘브라우저’에서 효율적으로 다룰 수 있도록 최적화된 정적 파일을 만드는 과정

브라우저 입장에서 중복되거나 불필요한 파일을 정리함

중복되거나 불필요한 파일을 정리한다는게 무슨 말이야?
// a.js
import { add } from './math.js'

// b.js
import { add } from './math.js'

a.js와 b.js 파일에서 math.js는 중복해서 두 번 import되고 있음 ← 브라우저 입장에서는 동일한 코드가 여러 번 로드됨!

번들러는 math.js 코드가 중복되었더라도 딱 한 번만 포함되도록 최적화하고,

dead code(쓰이지 않는 코드)를 제거하며, 사용하지 않는 export 제거(tree-shaking)한다!

 

 

- 번들링 예시

src/
├── App.js
├── utils/
│   ├── math.js
│   └── format.js
└── pages/
    └── Home.js

위와 같이 여러 개의 .js 파일이 있을 때, npm run build로 React 프로젝트를 실행하면

build/
├── index.html
└── static/
    └── js/
        ├── main.abcd1234.js   
	     모든 JS 코드가 여기에 압축되어 들어감
HTML index.html 루트 문서
JS main.[hash값].js js코드가 하나로 합쳐짐
CSS main.[hash값].css 스타일

 

- 번들링 목적

1. 파일 크기 줄이기

번들 파일은 번들링을 거치지 않은 원본 파일보다 파일의 크기가 작아지고 실행속도와 로딩속도가 빨라진다.

2. 애플리케이션 임의 조작 방지(난독화)

보안 목적보다는 (사용자가) 코드 분석/수정을 어렵게 만드는 것(완전한 보안 X)

3. 파일 단위의 js 모듈 관리

JS의 변수는 기본적으로 전역 범위를 가지므로 하나의 프로젝트 폴더에서 여러 개의 .js 파일이 있다면 서로 변수를 공유해 충돌이 발생할 수도 있음 → 번들러는 각 파일을 모듈로 캡슐화해서(각 파일을 독립된 scope로 감싸서) 서로 영향이 없도록 만듦

4. 의존성(import/export 구조 분석을 통한 파일 간의 관계) 자동 해결 및 모듈 연결

번들러가 전체 import 구조를 추적해서 의존성 트리 생성, 이 트리가 main.[hash값].js같은 하나의 번들로 묶임

 

2. Tree Shaking이 뭐야?

번들링 내용을 소개하며 'tree shaking'이라는 용어가 잠깐 등장했었죠?

트리 쉐이킹이란 간단히 얘기하자면 '사용되지 않는 코드(dead code)를 제거하는 작업입니다.

예를 들어 파일에 뭔가 import해놓고 실제로 사용은 안하는 경우가 종종 발생하는데, 프로젝트 빌드 시 webpack과 같은 번들러가 전체 의존성 트리를 분석하며 사용되지 않는 코드를 감지하고, 최종 번들에서 제거합니다. bundling으로 파일을 하나로 묶고, tree shaking을 통해 bundler 내부를 최적화하는 거죠! 

 

3. Typescript에서의 Enum?

에 대해 알아보기 전에, 

enum으로 선언했을 때, 일반 객체로 선언했을 때, as const로 선언했을 때 어떤 차이가 있는지부터 알아봐요!

1. enum으로 선언했을 때

enum Status {
	TODO = "todo",
    DOING = "doing",
    DONE = "done",
}

 

enum으로 선언해도 코드 동작에는 전혀 문제가 없습니다. 하지만 Typescript에서 Javascript로 코드를 트랜스파일 하는 과정에서 enum은 IIFE(Immediately Invoked Function Expression)으로 변환되고, 이런 구조는 번들러가 해당 코드가 사용되지 않는다고 판단하더라도 side effect가 있을 수 있다고 간주해 tree shaking 하지 못한다고 해요. 즉 성능 최적화에는 enum 사용이 바람직하지 않다는 뜻이죠!

 

2. 일반 객체로 선언했을 때

const Status = {
	TODO: "todo",
    DOING: "doing",
    DONE: "done",
}

 

자유롭게 수정할 수 있으며 확장도 가능하고 사용도 간편해서 저희가 일반적으로 가장 많이 사용하는 형태입니다.

as const와 비교했을 때 유니온 추출이 안된다는 단점이 있습니다.

 

3. 객체 + as const

const Status = {
  TODO: "todo",
  DOING: "doing",
  DONE: "done",
} as const;

객체에 as const를 붙이면 해당 객체를 enum과 같이 사용할 수 있어요!

더 정확히 얘기하자면, as const는 모든 속성을 read-only 리터럴 타입으로 고정해주는 역할을 합니다. Typescript는 위 코드를 아래와 같이 인식해요.

const Status: {
  readonly TODO: "todo";
  readonly DOING: "doing";
  readonly DONE: "done";
}

"todo"와 같이 정확한 리터럴 타입(값을 그대로 타입으로 간주)으로 추론되므로 타입 자체가 'todo'가 되며, 따라서 유니온 추출도 가능해집니다.(일반객체를 사용하면, 위 예시의 경우 TODO, DOING, DONE 모두 그냥 'string' 타입으로 간주돼요)

as const를 붙이면 모든 속성이 read-only가 되므로 동적으로 추가하거나 수정할 수 없다는 단점이 생기지만, enum처럼 쓰되 더 가볍고 정확한 타입 추론이 가능해서 실무에서 더 많이 사용된다고 해요. 추가적으로, enum은 트리 셰이킹이 안되지만 as const로 선언한 객체는 트리 셰이킹이 가능합니다.

 

4. 결론

조사 과정에서는 '그럼 Enum을 선언해놓고 무조건 사용한다면 그건 '사용하는' 코드니까 애초에 tree shaking의 고려 대상이 아니므로 enum을 사용해도 별 상관없는게 아닐까?' 하는 생각이 들기도 했습니다. 하지만 어쨌든, 

 

결론적으로 enum은 Javascript 객체로 트랜스파일 되는 과정에서 번들 크기를 약간 증가시킬 수 있고, 트리 셰이킹이 어려운 구조라는 점에서 굳이 enum을 사용할 필요는 없을 듯합니다. 실무에서는 대부분 as const 객체 리터럴을 사용하는 추세라고 하고, 타입 추론이 정밀하고 번들 최적화에도 유리하므로 저는 대부분의 상황에서는 as const를 사용할 것 같아요. 다만 as const는 역방향 매핑(value -> key)이 지원되지 않는 등 단점도 있으니 이런 상황을 마주하게 된다면 대체 방안으로 enum을 알아볼 것 같습니다!