Vite 번들 성능 개선기: 빌드 66% 단축, 번들 56% 절감
목차
요즘 들어 “코드를 잘 짜는 것”만큼이나 “빌드를 빠르게 만들고 번들을 가볍게 만드는 일”이 팀 생산성에 직접 영향을 준다는 걸 자주 느낀다. 이번에는 키오스크 웹 프로젝트에서 Vite 빌드 성능과 번들 사이즈를 꽤 크게 개선했던 과정을 정리해보려 한다.
핵심은 두 가지였다.
- 빌드 분석을 먼저 가능하게 만들기
- 사내 UI라이브러리 때문에 불필요하게 따라오는 대형 모듈을 stub으로 차단하기
문제: “쓰지도 않는데 번들에 왜 들어오지?”
프로젝트는 사내 공통 UI 라이브러리를 사용한다. 공통 라이브러리는 좋다. 다만 “공통”의 특성상 기능이 많고, 그 기능들이 의존하는 외부 패키지도 많다.
문제는 여기서 발생했다.
- 프로젝트에서 실제로 쓰지 않는 기능(예: 에디터, 지도, 애니메이션)도 import 경로나 의존성 그래프가 열려 있으면 빌드 과정에서 번들러가 끌고 들어오는 경우가 생긴다. 그래서 결과적으로 빌드가 느려지고 번들이 커지고 PR/배포 사이클이 점점 답답해졌다.
“번들 트리맵을 열자마자 느낀 위화감: 프로젝트 코드보다 사내 공통 UI라이브러리 덩어리가 훨씬 크다.”
visualizer 트리맵을 처음 열었을 때 솔직히 좀 당황했다. 기대했던 건 “프로젝트 코드가 어디가 무거운지”였는데, 실제로 눈에 들어온 건 프로젝트 번들보다 더 존재감이 큰 사내 공통 UI라이브러리 덩어리였다.
더 기이했던 건, 그 안에 이 프로젝트에서 전혀 쓰지 않는 기능(에디터/지도/애니메이션 등)로 추정되는 모듈들이 꽤 큰 비중으로 자리 잡고 있었다는 점이다. 즉, 성능 병목의 원인이 “내가 작성한 화면 코드”가 아니라 **공통 라이브러리의 옵션 기능이 의존성 그래프를 타고 따라 들어오며 생긴 ‘숨은 번들’**일 가능성이 높았다. 이 순간부터 최적화의 방향이 바뀌었다. _코드를 더 쪼개는 것보다, 애초에 들어오면 안 되는 모듈을 번들 단계에서 차단하는 게 먼저_였다.

목표: 숫자로 증명 가능한 개선
개선 전/후를 명확히 말하려면 기준이 필요해서, 동일한 방식으로 빌드했을 때 수치를 남겼다.
As-is
- 빌드 속도: 32.21s
- 번들 용량: 12.87MB
To-be
- 빌드 속도: 10.93s
- 번들 용량: 5.64MB
개선율은 이렇게 정리된다.
- 빌드 시간 66.1% 감소 (32.21s → 10.93s)
- 번들 용량 56.2% 감소 (12.87MB → 5.64MB)
접근: “측정 → 원인 → 차단/분리 → 재측정” 루프
최적화는 감으로 하면 실패하기 쉽다. 그래서 내가 정한 루프는 단순했다.
1)
번들 분석 환경 만들기
2)
불필요하게 들어오는 모듈을 확인
3)
들어오는 경로를 끊거나(chunk 분리/지연 로드)
4)
다시 빌드해서 수치와 결과를 확인- analyze 빌드 만들기: “일단 보여야 고칠 수 있다”
먼저 “번들이 왜 큰지”를 볼 수 있는 도구가 필요했다.
그래서 rollup-plugin-visualizer를 추가하고 vite build --mode analyze 형태로 실행하면 stats.html이 생성되도록 했다. 트리맵으로 보면 “어떤 덩어리가 크고, 어디에서 왔는지”가 한눈에 들어온다.
이 단계는 경험적으로도 가장 중요했다.
분석 없이 하는 최적화는 대부분 ‘움직인 것 같은 느낌’만 남는다.
- 이번 작업의 핵심: stub으로 “옵션 의존성”을 번들에서 제거
여기서부터가 이번 성능개선의 핵심이라고 볼 수 있다.
사내 UI라이브러리가 내부적으로(또는 하위 기능에서) 참조할 수 있는 모듈들 중 일부는, 우리 프로젝트에서는 전혀 쓰지 않는데도 번들에 포함될 여지가 있었다. 대표적으로 이런 계열들:
- tinymce (에디터)
- quill (에디터)
- lottie-web (애니메이션)
- maplibre-gl + 관련 CSS (지도)
- lodash (범용 유틸)
내가 선택한 방식은 “모듈을 삭제하거나 코드를 대규모로 고치기”가 아니라, 번들 단계에서 해당 모듈을 ‘빈 모듈’로 바꿔치기(stub)하는 것이었다.
즉:
- 빌드 중 특정 import를 만나면 실제 패키지를 resolve하지 않고 가짜(빈) 모듈로 치환해서 번들에 포함되지 않게 만든다.
이 방식은 공통 라이브러리를 사용하는 프로젝트에서 특히 유용하다.
- 공통 라이브러리 자체를 크게 뜯지 않아도 되고 “이 프로젝트에서는 쓰지 않는 기능”을 번들에서 강제로 제외할 수 있고 결과가 stats.html에서 명확히 드러난다
나는 Vite 플러그인 형태로 stubUnusedDeps를 만들고, JS 모듈과 CSS를 각각 규칙 기반으로 처리했다.
(지도 CSS 같은 경우는 빈 CSS로 교체)
- vendor 청크 전략: manualChunks로 캐시 효율/로딩 분리
번들 “총합”만 줄이는 것도 중요하지만, 실제 서비스에서는 캐시 효율이 매우 중요하다.
그래서 manualChunks를 설정해서 vendor를 성격별로 분리했다.
- vendor-react
- vendor-tanstack
- vendor-tinymce, vendor-maplibre-gl
- 나머지 vendor
이렇게 나누면:
- 변경이 거의 없는 덩어리는 더 오래 캐시되고 특정 기능이 필요 없으면 해당 청크 자체가 요청되지 않거나, 최소한 “변경 잦은 코드와 변경 적은 코드”가 뒤섞여 캐시가 깨지는 상황을 줄일 수 있다.
- 검증: “된 것 같음”이 아니라 “됐다”를 남기기
최적화는 종종 부작용을 만든다. 그래서 최소한 아래는 매번 확인했다.
- typecheck 통과
- analyze 빌드(build:analyze)가 정상적으로 완료되는지
- stats.html에서 의도한 모듈이 실제로 빠졌는지
- 빌드 시간/번들 크기 수치가 목표 방향으로 가는지
이 과정이 반복되면서 개선이 누적되었다.
결과: 수치로 말하기
최종적으로 내가 정리한 결과는 이렇다.
- 빌드 시간 66.1% 감소
- 번들 용량 56.2% 감소
그리고 내가 개인적으로 가장 만족한 지점은, 단순히 코드 스플리팅만 한 게 아니라
사내 UI라이브러리에서 유입되는 “불필요한 옵션 의존성”을 stub으로 차단해서 번들 자체를 더 건강하게 만들었다는 점이다.
회고: 다음엔 뭘 더 해볼까?
이번에 느낀 건,
- “나누는 것(code splitting)”도 중요하지만
- “애초에 들어오면 안 되는 걸 안 들어오게 하는 것(stub/resolve 차단)”이 더 강력한 경우가 많다는 것
추가로 더 욕심을 낸다면:
- vendor-hkmc-airlab가 여전히 크다면, 공통 라이브러리의 엔트리/sideEffects 설계 점검
- 실제 런타임 트래픽 기준으로 “어떤 청크가 언제 로드되는지”까지 추적(Performance 탭/Real User Monitoring)
- 특정 페이지/기능 단위 dynamic import로 더 공격적인 지연 로드
같은 것들을 이어갈 수 있을 것 같다.
마무리
이번 최적화는 내게 “번들”이 단순히 프론트엔드 빌드 산출물이 아니라, 팀 생산성과 사용자 경험을 동시에 좌우하는 제품의 일부라는 걸 다시 확인시켜준 작업이었다.
나처럼 사내 공통 라이브러리를 쓰는 프로젝트에서 “왜 안 쓰는 게 들어오지?”라는 의문을 가진 사람이 있다면, analyze로 보고 → stub으로 끊고 → chunk 전략을 잡는 흐름을 한 번 추천해보고 싶다.