'번들 크기를 100KB로 유지해라'
프론트엔드 성능 이야기를 하다 보면 꼭 한 번씩은 나오는 말입니다.
처음에는 그냥 관습처럼 들렸습니다. 그런데 Core Web Vitals를 파고들면서 이유가 생각보다 명확하다는 걸 알게 됐습니다.

왜 초기 번들 크기가 중요한가
핵심은 단순합니다. "사용자가 콘텐츠를 얼마나 빨리 보느냐" — 그 첫 번째 변수가 초기 번들 크기이기 때문입니다.
모든 사용자가 고속 인터넷을 쓰는 건 아닙니다. Google의 시뮬레이션 기준인 Slow 3G (1.5 Mbps)에서 단순 다운로드 시간만 계산해보면 이렇습니다.
번들 크기 다운로드만 JS 파싱 포함 체감
| 100 KB | ~0.5초 | ~0.8초 |
| 300 KB | ~1.6초 | ~2.5초 |
| 1 MB | ~5초 | ~8초 |
| 3 MB | ~16초 | ~20초+ |
숫자를 보면 느낌이 바로 옵니다. 그리고 저사양 Android 기기에서는 다운로드가 끝나더라도 JS 파싱 시간 자체가 병목이 됩니다. 다운로드가 끝났다고 끝난 게 아닌 것입니다.
Core Web Vitals와 직결된다
초기 번들 크기는 Google이 검색 순위 신호로 사용하는 Core Web Vitals 세 가지 모두에 영향을 줍니다.
LCP (Largest Contentful Paint) 가장 큰 콘텐츠가 렌더되는 시점입니다. 목표는 2.5초 이내. 번들이 크면 메인 스레드가 JS 파싱으로 막혀 렌더가 지연됩니다.
INP (Interaction to Next Paint) 클릭 등 상호작용 후 화면이 반응하기까지의 시간입니다. 목표는 200ms 이내. 불필요한 JS가 로드되면 메인 스레드가 바빠지면서 응답성이 떨어집니다.
CLS (Cumulative Layout Shift) 레이아웃 흔들림입니다. 불필요한 CSS나 폰트가 늦게 로드될 때 발생합니다. 번들 최적화는 JS뿐 아니라 CSS도 포함한다는 의미입니다.
100KB, 어디서 확인하나

엄밀히는 압축 후 JS 초기 전송 크기를 100KB 이하로 유지하는 것이 목표입니다.
Chrome DevTools → 네트워크 탭 → JS 필터를 켜면 하단 상태바에서 바로 확인할 수 있습니다.
267 kB / 2,382kB 전송됨 834 kB / 3,056kB 리소스
↑ ↑
JS 압축 전송 합산 JS 비압축(파싱) 크기 합산
(실제 네트워크 전송량) (브라우저가 파싱할 크기)
개별 파일의 크기 열에도 두 줄로 표시됩니다. 윗줄이 압축 전송 크기(Transfer Size), 아랫줄이 비압축 리소스 크기(Resource Size)입니다.
참고로 Google Lighthouse 기준으로 "Avoid enormous network payloads" 경고가 뜨는 기준이 1.6 MB이고, 권장 초기 로드는 비압축 200 KB 이하입니다. 여기서 이미지를 제외한 JS + CSS만 따지면 실무적으로 100 KB 이하를 목표로 잡는 게 안전한 기준선이 됩니다.
프레임워크를 쓰면 100KB는 이미 초과된다
여기서 현실적인 문제가 생깁니다. 프레임워크 자체가 이미 100KB를 넘기기 때문입니다.
프레임워크 최소 런타임 (gzip) 비고
| Vue 3 | ~22 KB | 가장 가볍다 |
| React + ReactDOM | ~42 KB | 런타임만 |
| Next.js | ~90~130 KB | 프레임워크 + React + 라우터 포함 |
| Next.js + 기본 청크 | ~150~250 KB | 실제 페이지 진입 시 |
Next.js는 아무것도 안 짜도 100 KB를 넘깁니다. 그러면 이 기준은 무의미한 걸까요?
그렇지 않습니다. 100KB의 진짜 의미는 이겁니다.
프레임워크 런타임은 캐싱되고 반복 방문 시 재다운로드가 없으므로, 앱이 추가로 얹는 코드를 최소화하라는 맥락입니다.
그래서 실무에서는 목표를 이렇게 재정의합니다.
- 프레임워크 런타임 → 캐시 전략으로 분리 (long-term cache)
- 페이지별 앱 코드 → 50 KB 이하 (압축 후)
- 전체 초기 전송 합산 → 200 KB 이하 (압축 후)
Next.js에서의 청크 구조
Next.js는 번들을 자동으로 이렇게 쪼갭니다.
framework chunk → react, react-dom (캐시 고정)
commons chunk → 여러 페이지가 공유하는 코드
page chunk → 해당 페이지 전용 코드 ← 핵심 관리 대상
DevTools에서 /_next/static/chunks/ 요청을 기준으로 보면 아래처럼 정리됩니다.
청크 종류 관리 여부 목표
| framework-xxx.js | 건드리지 않음 | 브라우저 캐시 활용 |
| main-xxx.js | 간접 관리 | 50 KB 이하 유지 |
| pages/[route]-xxx.js | 직접 관리 | 30 KB 이하 목표 |
| chunks/xxx.js (공유) | 분석 후 관리 | 불필요 의존성 제거 |
Next.js 환경의 현실적인 기준: 전체 200 KB 이하 / 앱 코드만 100 KB 이하
Vue + Vite에서는 어떻게 다른가
Vue 3 런타임은 ~22 KB로 React(~42 KB)의 절반 수준입니다. Vue Router + Pinia까지 더해도 이렇습니다.
패키지 압축 크기
| vue 런타임 | ~22 KB |
| vue-router | ~11 KB |
| pinia | ~3 KB |
| 합산 | ~36 KB |
React + ReactDOM만 42 KB인데 Vue는 생태계 핵심 3종을 묶어도 36 KB입니다. 출발선 자체가 다릅니다.
Vite 기준 초기 번들 구조는 이렇게 만들어집니다.
vendor chunk → vue, vue-router, pinia (캐시 고정)
app chunk → 실제 앱 코드
page chunks → 라우트별 lazy load
Vue + Vite에서의 현실적인 목표입니다.
구분 크기 목표
| vendor chunk (vue + router + pinia) | ~36 KB — 캐시 활용 |
| 앱 공통 코드 | 30 KB 이하 |
| 페이지별 청크 | 20 KB 이하 |
| 전체 초기 전송 | ~150 KB 이하 |
Vue는 SFC 구조상 템플릿이 컴파일 타임에 최적화된 렌더 함수로 변환되기 때문에 런타임 오버헤드가 작습니다. 그리고 Vue 3 Composition API는 사용한 기능만 번들에 포함되도록 tree shaking이 잘 적용됩니다.
Vuetify를 쓰고 있다면
현재 프로젝트에는 Vuetify를 UI 라이브러리로 사용하고 있었습니다. 문제는 Vuetify가 라이브러리 중에서도 무거운 축에 속한다는 것입니다.
항목 압축 크기
| Vuetify 전체 import | ~280~350 KB |
| Vuetify treeshake 적용 | ~80~150 KB |
| Vue 런타임 포함 합산 | ~100~170 KB |
Vue 런타임 + Vuetify까지 합산하면 초기 진입에서만 ~170 KB를 라이브러리가 점유합니다. 150 KB 전체 목표를 라이브러리만으로 초과해버리는 구조입니다.
이를 막으려면 treeshaking + 자동 import 설정이 필수입니다.
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vuetify, { transformAssetUrls } from 'vite-plugin-vuetify'
export default defineConfig({
plugins: [
vue({ template: { transformAssetUrls } }),
vuetify({ autoImport: true }), // ← 이게 핵심
],
})
// main.js
import { createVuetify } from 'vuetify'
// ❌ 이렇게 하면 전체 번들이 포함됩니다
import * as components from 'vuetify/components'
// ✅ autoImport: true 설정 시 사용한 컴포넌트만 자동 포함됩니다
const vuetify = createVuetify({
components,
})
그렇게 설정하더라도, Vue + Vite + Vuetify 조합이라면 목표 자체를 200~220 KB로 상향 조정하는 것이 현실적입니다.
기본 목표 Vuetify 사용 시
| Vue + Vite 전체 | 150 KB | 200~220 KB |
| 앱 코드 여유 | 80 KB | 50~70 KB |
앱 코드에 쓸 수 있는 여유가 80 KB에서 50 KB 수준으로 줄어듭니다. 기능이 늘어날수록 이 여유를 지키기가 점점 빡빡해집니다.
그래서 Vuetify를 걷어내기로 했습니다
기존 프로젝트는 대부분 사내에서 사용하고 있었지만 전면적으로 사업을 진행하기로 했습니다. B2B, B2C 혹은 그 어떤 방식이던 그 순간 Vuetify는 부담이 됩니다.
결국 Vuetify를 걷어내고 더 가벼운 대안으로 교체하는 방향으로 결정했습니다.
어떤 라이브러리로 갈아탈지, 그 과정은 다음 글에서 이어서 정리해보겠습니다.
'tech epilogue' 카테고리의 다른 글
| ChunkLoadError 해결기 (0) | 2026.05.15 |
|---|---|
| 번들 크기 100KB를 찾아서(2) (0) | 2026.05.07 |
| 사용자들이 신청 완료 화면까지 볼 수 있도록! (0) | 2026.04.27 |
| 3분 넘게 걸리던 배포, 1분 30초대로 깎아낸 Bitbucket Pipeline 최적화하기 (0) | 2026.04.22 |
| 우리도 구글 검색 1페이지에 나오고 싶다(2) (0) | 2025.10.17 |