번들 사이즈 예산을 정하지 않으면 성능은 어떻게 되나?

번들 크기 예산이 없으면 성능 개선은 문제 발생 후의 임시 대응이 된다. 초기 번들(Initial) < 170KB gzipped, 경로 단위(Per Route) < 50KB gzipped, 전체 JavaScript < 300KB, 페이지 전체 < 1.5MB를 기준으로 설정하면 4G 모바일에서 3초 이내 로딩을 현실적으로 담보할 수 있다. 예산이 없으면 번들은 한 번에 200KB씩 커지고, 측정하는 시점에 이미 구조적 개선(코드 스플리팅, 이미지 포맷)이 불가능해진다.

  • 초기 로드 번들 제약 170KB → 데스크톱/모바일 구분 없이 첫 3초 문턱 대응 가능
  • 경로별 청크 50KB 이하 → 라우트 전환 시 추가 대기(Added Latency) 50ms 이내
  • 이미지 최적화 먼저, 코드 스플리팅 둘째, 렌더 차단 자원 제거 셋째의 순서 준수 필수
  • RUM 데이터와 Lighthouse CI 자동화로만 회귀 방지 가능 — 수동 점검은 즉시 무너짐

번들 크기 제약이 구체적으로 무엇을 의미하나?

번들 사이즈 예산은 단순한 수치가 아니라 사용자 환경에 직결된 네트워크 대역폭 제약이다.

초기 번들 < 170KB gzipped는 4G LTE(다운로드 약 10Mbps)에서 약 140ms 내에 다운로드 가능한 크기다. 이는 HTML 파싱, CSS 렌더 블록, JavaScript 실행까지 포함한 3초 이내 로딩의 30% 정도를 차지한다. 초기 번들이 250KB를 넘으면 네트워크 대기 시간(Network Round Trip)이 누적되고, JavaScript 파싱 시간이 추가로 300ms 이상 늘어난다.

경로 단위 < 50KB gzipped는 라우트 기반 코드 스플리팅의 목표 청크 크기다. 사용자가 /products 페이지에 진입할 때 추가로 다운로드하는 번들이 50KB를 초과하면, 라우트 전환 느낌이 즉시 체감된다. 실제로 50KB 청크는 4G에서 40ms, 100KB 청크는 80ms의 추가 지연을 낳는다. INP(Interaction to Next Paint) < 150ms 목표를 감안하면 라우트 이동 후 상호작용 반응까지 총 시간이 200ms를 넘기 쉽다.

전체 JavaScript < 300KB는 메인 스레드 파싱 시간 제약이다. 번들이 300KB를 넘으면 V8 엔진의 파싱 시간이 800ms를 초과할 가능성이 높고, 이는 TTI(Time to Interactive)를 악화시킨다. 특히 모바일 CPU는 데스크톱보다 3배 느리므로 모바일 기준 300KB가 실질적인 상한선이다.


왜 이미지 최적화부터 시작하고, 코드 스플리팅은 뒤에 하나?

실무에서 본 최적화 순서는 투자 대비 이득(ROI) 때문에 이 순서로 고정되어 있다.

이미지는 대개 전체 페이지 무게의 5070%를 차지한다. 특히 LCP(Largest Contentful Paint) 이미지는 보통 200400KB이고, 이를 WebP(JPEG 대비 30% 축소) 또는 AVIF(50% 축소)로 변환하는 것은 비용이 거의 없다. 단순 포맷 변환만으로 LCP 이미지 800ms 개선, 전체 페이지 로딩 200ms 개선을 본다. 렌더 차단 시간 제거보다 먼저 진행해야 하는 이유는 대역폭 절약이 1순위이기 때문이다.

코드 스플리팅은 번들을 여러 청크로 쪼개는 작업이고, 이는 빌드 설정 변경, 동적 import 리팩터링, 테스트를 요구한다. 초기 번들이 이미 300KB 이상이면 스플리팅으로 초기 로드를 150KB까지 줄일 수 있지만, 이미지가 이미 800KB라면 스플리팅은 근본 해결이 아니다. 따라서 먼저 이미지를 200KB 아래로 압축한 후, 남은 예산 안에서 코드 스플리팅을 적용하는 것이 효율적이다.

렌더 차단 자원(Critical CSS, 비필수 스크립트)은 마지막에 다루는 이유는 영향 범위가 가장 좁기 때문이다. CSS를 <style> 태그로 인라인 처리하면 200~300ms 개선이 가능하지만, 이를 위해 빌드 파이프라인을 수정하고 CSS 분리 전략을 재설계해야 한다. 이미지와 코드 크기를 먼저 통제한 후 렌더 차단 제거는 추가 보험으로 기능한다.


번들 예산이 정해진 후, 회귀는 어떻게 방지하나?

예산이 깨지는 순간을 감지하지 못하면 번들은 다시 팽창한다.

첫째, RUM(Real User Monitoring) 기반 모니터링이다. Lighthouse는 실험실 환경(Lab)에서 일관된 네트워크와 디바이스를 가정하지만, 실제 사용자는 3G, 저가 스마트폰, 느린 CPU에서 접근한다. Google Analytics 또는 Web Vitals 라이브러리를 심어 LCP, INP, CLS를 수집하고, Search Console의 CrUX 데이터와 비교하면 배포 후 2~4주 뒤 실제 성능 변화를 본다. 번들이 150KB에서 180KB로 증가했을 때 실제 사용자 환경에서 LCP가 2.0s에서 2.3s로 악화되는 패턴을 추적할 수 있다.

둘째, Lighthouse CI 자동화로 배포 전 즉시 차단한다. GitHub Actions 또는 CI 파이프라인에 Lighthouse CI를 통합하면, PR 생성 시마다 자동으로 번들 크기, LCP, INP를 측정하고, 초기 번들이 170KB를 넘거나 경로 청크가 50KB를 초과하면 배포를 경고하거나 차단한다. 예를 들어, 팀원이 라이브러리를 추가했을 때 번들 크기 증가분이 즉시 CI 로그에 표시되고, 번들 "차이(delta)"를 수동 승인하거나 코드를 수정하게 한다.

셋째, Chrome DevTools Coverage 패널로 사용하지 않는 코드를 주기적으로 점검한다. 실무에서 보면 CSS 프레임워크(Tailwind, Bootstrap)를 도입한 후 사용 안 하는 클래스가 3040% 남아있는 경우가 흔하다. Tree Shaking과 sideEffects: false 설정으로 데드코드를 제거하고, @supports 쿼리로 불필요한 폴리필을 조건부 로딩하면 1020% 번들 감소를 본다.


흔한 오해: "LCP 이미지에 loading='lazy'를 붙이면 성능이 더 좋아진다"

이것은 실무에서 본 가장 흔한 실수다.

LCP 이미지는 유저가 페이지를 열자마자 보는 Hero 이미지나 상단 배너다. 이 이미지에 loading="lazy"를 설정하면 브라우저는 이미지가 화면에 진입할 때까지 로딩을 미룬다. 이는 초기 로드 시 불필요한 이미지(footer, 하단 제품 목록)를 로딩하지 않으려는 의도로는 좋지만, LCP 이미지에 적용하면 다음이 일어난다:

  1. HTML 파싱 완료 후 LCP 이미지 요청이 시작됨 (즉, 100ms 늦음)
  2. 이미지 다운로드 시간 추가 (LCP 지연)
  3. 최종 LCP = 원래 기대 LCP + 100~200ms

실제로 초기 번들에만 loading="lazy"를 제거하고 나머지는 유지해도 LCP가 2.5s에서 2.0s로 개선된다. 올바른 방법은 LCP 이미지에 fetchpriority="high" + <link rel="preload">를 명시하고, loading="lazy"는 폴드 아래 이미지에만 적용하는 것이다.

또 하나의 오해는 "이미지 포맷 변환은 선택사항"이라는 생각이다. 실제로는 JPEG 이미지 100KB를 WebP로 변환하면 70KB가 되고, 이는 네트워크 시간 120ms 절약이다. 모바일 4G에서는 이 120ms가 LCP에 직결된다.


번들 예산을 어떻게 수립하고, 언제 위반하는 걸 허용할까?

번들 예산은 고정이 아니라 프로젝트 성숙도와 사용자층에 따라 조정된다.

신규 프로젝트(초기): 초기 번들 < 170KB gzipped 기준을 그대로 따르는 게 낫다. 아직 레거시 코드, 불필요한 의존성이 축적되지 않았으므로, 처음부터 예산 안에서 설계하는 게 훨씬 쉽다.

성숙 단계(6개월~): 초기 번들 170KB 유지하되, 경로별 청크를 허용한다. 예를 들어, /admin 대시보드는 관리자 5%만 접근하므로 청크 크기 제한을 100KB로 완화할 수 있다.

특수 경우(e-commerce, 대규모 제품군): 초기 번들은 120KB로 더 엄격히 하되, 라우트별 청크는 100KB까지 허용한다. 다시 말해, 홈페이지 로드는 빨리, 카테고리 진입은 약간의 대기를 감수한다는 트레이드오프다.

위반을 허용하는 경우: 번들 증가가 명확한 비즈니스 가치를 낼 때만. 예를 들어, 결제 흐름에 3D Secure 인증 라이브러리(80KB)를 추가하는 것은 필수이므로 허용한다. 하지만 분석 도구, 채팅 위젯, 광고 태그는 순서를 정해 하나씩만 추가하고, 각각의 번들 영향을 측정 후 승인한다.


번들 예산 설정부터 회귀 방지까지, 체크리스트

번들 사이즈 예산은 감이 아니라 제약과 측정의 순환이다.

  • 예산 수립: 초기 번들 170KB, 경로 청크 50KB, 전체 JavaScript 300KB, 페이지 무게 1.5MB — 4G 모바일 기준 3초 이내 로딩 담보
  • 최적화 순서: 이미지(LCP 우선, WebP/AVIF 포맷, loading="lazy" 폴드 아래만) → 코드 스플리팅(Route/Component 기반) → 렌더 차단 제거(Critical CSS 인라인, defer/async)
  • 회귀 방지: RUM 기반 실제 사용자 데이터 추적 + Lighthouse CI 자동화로 배포 전 차단, 주기적 Coverage 점검으로 데드코드 제거
  • 위반 판단: 명확한 비즈니스 가치(결제 보안, 핵심 기능)가 있을 때만 허용, 분석/마케팅 도구는 번들 영향을 측정 후 우선순위 결정
  • 측정 루프: 개선 전후 구체 수치(LCP, INP, CLS ms 단위) 기록 → 배포 후 2~4주 뒤 CrUX 데이터로 실제 사용자 성능 검증 → 회귀 발생 시 즉시 이전 버전으로 롤백