Vite 번들 성능 개선기: 빌드 66% 단축, 번들 56% 절감

최정은
2025/12/31

요즘 들어 “코드를 잘 짜는 것”만큼이나 “빌드를 빠르게 만들고 번들을 가볍게 만드는 일”이 팀 생산성에 직접 영향을 준다는 걸 자주 느낀다. 이번에는 키오스크 웹 프로젝트에서 Vite 빌드 성능과 번들 사이즈를 꽤 크게 개선했던 과정을 정리해보려 한다.

핵심은 두 가지였다.

  1. 빌드 분석을 먼저 가능하게 만들기
  2. 사내 UI라이브러리 때문에 불필요하게 따라오는 대형 모듈을 stub으로 차단하기

문제: “쓰지도 않는데 번들에 왜 들어오지?”

프로젝트는 사내 공통 UI 라이브러리를 사용한다. 공통 라이브러리는 좋다. 다만 “공통”의 특성상 기능이 많고, 그 기능들이 의존하는 외부 패키지도 많다.

문제는 여기서 발생했다.

  • 프로젝트에서 실제로 쓰지 않는 기능(예: 에디터, 지도, 애니메이션)도 import 경로나 의존성 그래프가 열려 있으면 빌드 과정에서 번들러가 끌고 들어오는 경우가 생긴다. 그래서 결과적으로 빌드가 느려지고 번들이 커지고 PR/배포 사이클이 점점 답답해졌다.

“번들 트리맵을 열자마자 느낀 위화감: 프로젝트 코드보다 사내 공통 UI라이브러리 덩어리가 훨씬 크다.”

visualizer 트리맵을 처음 열었을 때 솔직히 좀 당황했다. 기대했던 건 “프로젝트 코드가 어디가 무거운지”였는데, 실제로 눈에 들어온 건 프로젝트 번들보다 더 존재감이 큰 사내 공통 UI라이브러리 덩어리였다.

더 기이했던 건, 그 안에 이 프로젝트에서 전혀 쓰지 않는 기능(에디터/지도/애니메이션 등)로 추정되는 모듈들이 꽤 큰 비중으로 자리 잡고 있었다는 점이다. 즉, 성능 병목의 원인이 “내가 작성한 화면 코드”가 아니라 **공통 라이브러리의 옵션 기능이 의존성 그래프를 타고 따라 들어오며 생긴 ‘숨은 번들’**일 가능성이 높았다. 이 순간부터 최적화의 방향이 바뀌었다. _코드를 더 쪼개는 것보다, 애초에 들어오면 안 되는 모듈을 번들 단계에서 차단하는 게 먼저_였다.

%E1%84%89%E1%85%B3%E1%84%8F%E1%85%B3%E1%84%85%E1%85%B5%E1%86%AB%E1%84%89%E1%85%A3%E1%86%BA_2025-12-31_%E1%84%8B%E1%85%A9%E1%84%8C%E1%85%A5%E1%86%AB_8.15.19.png


목표: 숫자로 증명 가능한 개선

개선 전/후를 명확히 말하려면 기준이 필요해서, 동일한 방식으로 빌드했을 때 수치를 남겼다.

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
다시 빌드해서 수치와 결과를 확인
  1. analyze 빌드 만들기: “일단 보여야 고칠 수 있다”

먼저 “번들이 왜 큰지”를 볼 수 있는 도구가 필요했다.

그래서 rollup-plugin-visualizer를 추가하고 vite build --mode analyze 형태로 실행하면 stats.html이 생성되도록 했다. 트리맵으로 보면 “어떤 덩어리가 크고, 어디에서 왔는지”가 한눈에 들어온다.

이 단계는 경험적으로도 가장 중요했다.

분석 없이 하는 최적화는 대부분 ‘움직인 것 같은 느낌’만 남는다.

  1. 이번 작업의 핵심: stub으로 “옵션 의존성”을 번들에서 제거

여기서부터가 이번 성능개선의 핵심이라고 볼 수 있다.

사내 UI라이브러리가 내부적으로(또는 하위 기능에서) 참조할 수 있는 모듈들 중 일부는, 우리 프로젝트에서는 전혀 쓰지 않는데도 번들에 포함될 여지가 있었다. 대표적으로 이런 계열들:

  • tinymce (에디터)
  • quill (에디터)
  • lottie-web (애니메이션)
  • maplibre-gl + 관련 CSS (지도)
  • lodash (범용 유틸)

내가 선택한 방식은 “모듈을 삭제하거나 코드를 대규모로 고치기”가 아니라, 번들 단계에서 해당 모듈을 ‘빈 모듈’로 바꿔치기(stub)하는 것이었다.

즉:

  • 빌드 중 특정 import를 만나면 실제 패키지를 resolve하지 않고 가짜(빈) 모듈로 치환해서 번들에 포함되지 않게 만든다.

이 방식은 공통 라이브러리를 사용하는 프로젝트에서 특히 유용하다.

  • 공통 라이브러리 자체를 크게 뜯지 않아도 되고 “이 프로젝트에서는 쓰지 않는 기능”을 번들에서 강제로 제외할 수 있고 결과가 stats.html에서 명확히 드러난다

나는 Vite 플러그인 형태로 stubUnusedDeps를 만들고, JS 모듈과 CSS를 각각 규칙 기반으로 처리했다.

(지도 CSS 같은 경우는 빈 CSS로 교체)

  1. vendor 청크 전략: manualChunks로 캐시 효율/로딩 분리

번들 “총합”만 줄이는 것도 중요하지만, 실제 서비스에서는 캐시 효율이 매우 중요하다.

그래서 manualChunks를 설정해서 vendor를 성격별로 분리했다.

  • vendor-react
  • vendor-tanstack
  • vendor-tinymce, vendor-maplibre-gl
  • 나머지 vendor

이렇게 나누면:

  • 변경이 거의 없는 덩어리는 더 오래 캐시되고 특정 기능이 필요 없으면 해당 청크 자체가 요청되지 않거나, 최소한 “변경 잦은 코드와 변경 적은 코드”가 뒤섞여 캐시가 깨지는 상황을 줄일 수 있다.
  1. 검증: “된 것 같음”이 아니라 “됐다”를 남기기

최적화는 종종 부작용을 만든다. 그래서 최소한 아래는 매번 확인했다.

  • 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 전략을 잡는 흐름을 한 번 추천해보고 싶다.