FE 번들 사이즈 다이어트 도전기 (번들 사이즈 1.7MB → 216KB, 87% 감소)

2026년 6월 15일

#front

현재 제가 담당하고 있는 서비스는 글로벌 서비스로, 다양한 국가에서 서비스가 배포되어 있습니다. 하지만 항상 해외 세미나 등에서 저희 서비스가 매우 느리게 동작하는 아주 큰 이슈가 존재했어요.

이는 서비스의 신뢰와 사용성을 해쳤고, 이를 해결해야한다는 과제가 있었습니다.

해결을 위한 고민 과정에서 그 원인 중 하나는 큰 번들 사이즈낮은 Lighthouse 점수가 아닐까? 라는 생각을 했습니다.

특히

“첫 화면에 진입하는 데 왜 이렇게 오래 걸리지?”

라는 질문으로 FE 성능 개선 작업에 도전하였습니다. 결과적으로는 Lighthouse 점수를 67점에서 90점 이상으로 끌어올렸습니다.


작업 동기

서비스 초기 진입 시 공통으로 로드되는 JS 번들(First Load JS shared by all)이 1.7MB에 달하고 있었습니다.

또한 크롬이 제공하고 있는 Lighthouse로 트래픽이 높은 주요 화면(대시보드, 주문 목록, 주문 상세, 로그인 등)을 측정해보니 상황은 생각보다 심각했습니다.

지표 개선 전 목표 상태
Performance Score 67.73 ≥90 🔴
LCP 7.48s <2.5s 🔴
TTI 7.52s <3.8s 🔴
FID (Max Potential) 158ms <100ms 🟡

LCP가 7초, TTI가 7초 라는 건 사용자가 버튼 하나를 클릭하기까지 7초 이상 기다려야 한다는 것이었습니다. 이 로딩 과정에서 이탈이 발생하는 것은 어쩌면 당연합니다.


목표 설정

개선 작업을 본격적으로 진행하기 전, 명확한 KPI를 먼저 정의하였습니다.

  • First Load JS shared by all: 1.7MB → 200KB 이하 (약 88% 절감)
  • Lighthouse Performance Score: 80점 이상 유지
  • LCP, TTI 등 Core Web Vitals “Good” 수준 달성

번들 사이즈는 단순히 숫자가 아닌, 유저가 서비스 화면에 도달하기까지 걸리는 시간과 전반적인 성능을 직접적으로 반영하는 지표입니다. 이를 개선하면 사용자의 체감 로딩 속도 향상 및 사용자 경험의 개선으로 이어집니다.

번들 사이즈를 줄인다면, LCP, FCP, TTI에 영향을 주고, 결국 사용자 이탈률에 긍정적인 영향을 줄 것입니다.


접근 방법 : 번들을 먼저 분석하기

ANALYZE=true pnpm build로 세팅하여 번들 트리맵을 분석했습니다. 트리맵에서는 세 가지 크기를 볼 수 있는데요,

  • Stat Size: 원본 크기 (참고용)
  • Parsed Size: 브라우저가 파싱하는 크기 (중요 ✨)
  • Gzipped Size: 실제 전송 크기 (가장 중요 ✨✨✨)

분석 결과, 클라이언트 초기 번들 크기는 2.48MB였습니다. 눈에 띄게 큰 영역들은 팀 내에서 커스텀하여 사용 중인 Icon 컴포넌트, PDF 라이브러리, 분석 SDK(Amplitude, Datadog), shared/ui/index 순이었습니다.


진행한 작업들 : 각 작업들의 진행 배경과 작업 내용

1. Lottie 라이브러리 제거 (-130KB)

스피너 구현에 Lottie Player를 사용하고 있었는데, 단순 로딩 인디케이터 하나를 위해 무거운 Lottie 라이브러리 전체를 번들에 포함시키고 있었습니다.

Lottie 기반 스피너를 모두 shared/ui에서 제공하는 경량 Spinner 컴포넌트로 교체했습니다.

결과: First Load JS 1.7MB → 1.57MB (-130KB)


2. 아이콘 동적 로딩 (-390KB) -> 가장 큰 영향 ✨

Icon 컴포넌트는 내부적으로 318개 이상의 아이콘을 index.ts에서 한 번에 export하고 있었습니다. 단 1개 아이콘만 사용해도 전체 318개 아이콘이 번들에 포함되는 구조였습니다. Gzipped 기준으로 397KB 규모였습니다.

이전에는 사용하는 것과 무관하게 전체 아이콘 번들에 포함되는 구조였다면, 개선 후엔 실제 해당 컴포넌트에서 사용하는 아이콘만 런타임에 로드되도록 개선하고자 했습니다.

Icon 컴포넌트 내부에서 useEffect와 동적 import()를 활용해, 실제 사용하는 아이콘만 런타임에 로드하도록 변경했습니다.

내부 구현 중 핵심 코드는 아래와 같습니다.

useEffect(() => {
  import(`../../../asset/icon/${name}`).then((module) => {
    setSvgIcon(() => module.default);
  });
}, [name]);

결과: First Load JS 1.57MB → 1.18MB (-390KB)


3. PDF 라이브러리 트리쉐이킹 + Dynamic Import (-380KB)

@react-pdf/renderer, pdfjs-dist는 PDF 관련 화면에서만 필요한데, 모노레포 구조상 공통 컴포넌트들이 위치한 디렉터리 ui/shared 의 barrel export 구조 때문에 모든 페이지의 초기 번들에 포함되고 있었습니다.

// shared/ui/src/index.ts
export { default as PDFHeaderUI } from "./PdfUI/PDFHeaderUI"; // ❌
export { default as PDFTableUI } from "./PdfUI/PDFTableUI"; // ❌
export { default as PdfPreviewUI } from "./PdfUI/PdfPreviewUI"; // ❌

Barrel export가 왜 문제일까?

Webpack(Next.js)은 export * 구조에서 사이드이펙트 여부를 판단하기 어려워 보수적으로 처리합니다. 결국 사용하지 않는 컴포넌트도 번들에 포함될 가능성이 높아지고, 번들사이즈도 함께 높아지게 됩니다.

작업 내용

  1. @ui/index에서 PDF 관련 컴포넌트를 분리해 별도 진입점(@ui/pdf)으로 이동
  2. PDF 페이지에서는 next/dynamic으로 지연 로딩 적용

결과: First Load JS 1.57MB → 1.19MB (-380KB)


4. Amplitude, Datadog 분석 SDK 동적 로딩 (-110KB)

Amplitude SDK와 Datadog RUM SDK가 앱 초기화 시점에 정적으로 import되어 모든 페이지의 초기 번들에 포함되어 있었습니다. 분석 SDK는 사용자 행동 추적용으로, 초기 렌더링에 필요한 요소가 아니었습니다.

따라서, 두 SDK 모두 동적 import 방식으로 전환했습니다.

다만 동적 로딩 도입 시 “SDK가 로드되기 전에 발생한 이벤트를 잃어버리지 않을까?”라는 문제가 생겼고, 해당 문제는 이벤트 큐 버퍼링 시스템을 구축하여 해결했습니다.

SDK 로드 전: 이벤트를 큐에 버퍼링 (FIFO)
SDK 로드 완료: flushQueue()로 순서대로 전송
SDK 로드 후: 큐를 거치지 않고 바로 전송

이 방식으로 이벤트 유실 없이 데이터 수집의 무결성을 유지하면서도 초기 번들에서 SDK를 제거할 수 있었습니다.

결과: 초기 번들 110KB 감소


5. shared/ui Barrel Export 구조 개선 (-209KB)

shared/ui/index.ts는 161개 이상의 컴포넌트를 한 번에 export하고 있었습니다.

트리맵에서 ui/src/index 파일 하나가 3MB를 차지하고 있었고, OrderUI(536KB), OrderDetailUI(392KB), LinkTalkContent(228KB) 같은 피처 성격의 무거운 컴포넌트들이 성격이 다른 페이지에서도 번들에 포함되고 있었습니다.

작업 내용

이를 해결하기 위해, 피처 성격의 컴포넌트들을 도메인별로 분리해 별도 진입점으로 이동했습니다 (order, auth, linktalk, pickup, remake 등).

또한 shared/ui/package.json"sideEffects": ["**/*.css"]를 추가해 CSS를 제외한 나머지 모듈에 트리쉐이킹이 적용되도록 설정했습니다.

결과: First Load JS 691KB → 482KB (-209KB)


6. Swiper + 팝업 컴포넌트 Dynamic Import (-150KB)

Swiper는 2개 화면에서만 사용됨에도 불구하고 메인 번들에 약 60KB가 포함되어 있었습니다.이러한 성격의 컴포넌트들은 동적로딩을 적용하여 코드 스플리팅을 적용했습니다.

const DisplayPopup = dynamic(() => import("../Display/DisplayPopup"), {
  ssr: false,
});

결과: 150KB 이상 절약


7. 웹폰트 최적화 (FCP 개선)

이번 번들 사이즈 줄이기 프로젝트를 진행하며, Datadog 를 분석하여 Lighthouse 가 아닌 실제 유저 환경에서의 지표 또한 함께 분석하였습니다.

Datadog 분석 결과, Loading Time이 가장 길게 소요되는 에셋은 woff 폰트 파일들이었습니다. 5개 이상의 웹폰트를 사용하고 있었고, 다국어 지원 폰트이기 때문에 파일 용량이 높았습니다.

이를 해결해보고자, 라틴(영문 + 기호) 웹폰트를 활용해 사용하는 폰트 파일을 최소화하고, 현재 디자인시스템에서 사용하고 있는 400, 700 weight만 유지하도록 정리했습니다.

결과: FCP 개선에 직접적으로 기여


최종 결과

지표 1.56.0 (개선 전) 1.58.0 1.60.0 변화
First Load JS 1.7MB - 216KB -87.6%
Performance Score 67.73 ≥90 ≥90 +22점 이상
LCP 7.48s 1.51s 1.62s -78%
TTI 7.52s 1.52s 1.62s -80%
TBT 88ms 2ms 0ms -100%
FID 158ms 51ms 21ms -86%

마무리

이번 작업은 회사에서 담당하고 있는 피처 개발 외에 문제점을 스스로 찾고, 고민하고, 해결한 경험이었기 때문에 성취감, 회사를 위해 기여했다는 것이 느껴졌습니다. 앞으로도 능동적으로 문제를 찾고 해결하는 과정을 쌓으면서 성장하고 싶네요 🥰

이번 작업을 통해 배운점과 생각들을 정리해보았습니다.

1. 문제 발견 > 문제 분석 > 최적화 작업

번들 분석, 문제 정의를 하지 않고 최적화 작업을 진행한다는 것은 의미없는 결과물을 낳을 수도 있습니다. 번들을 구성하는 트리맵을 먼저 확인하며, 가장 번들 사이즈에 악영향을 주는 것부터 접근하며 해결하는 것이 도움이 되었습니다.

2. Barrel Export는 편리하지만, 번들 사이즈에 악영향

import { Button } from "@ui" 한 줄이 내부적으로 수백 개의 컴포넌트를 번들에 끌어들일 수 있습니다. 라이브러리 규모가 커질수록 export 구조에 대한 설계가 중요하다는 것을 깨달았습니다.

3. Dynamic Import는 “초기 로딩에 필요한가?”를 기준으로 판단

사실 이전까지 Dynamic Import 를 정확히 어느 시점에 사용해야할지 미지수였습니다. 이번 작업을 통해 깨달은 것 같습니다.

번들 사이즈에서 많은 영역을 차지하고 있던 Swiper, PDF, 분석 SDK 모두 “지금 당장 필요한가?”라는 질문에 “아니오”였습니다. 필요한 시점에 로드하면 초기 번들에서 제거할 수 있고, 그 방법은 동적 로드를 활용하는 것이었습니다.

4. 번들 사이즈 줄이기는 곧 UX 지표 점수 올리기

216KB 라는 숫자도 중요하지만, LCP가 7초에서 1.6초로 줄고 TTI가 80% 단축되었다는 것이 FE 개발자로서의 큰 성과입니다. 번들 최적화는 결국 사용자 경험 개선입니다.


성능에 영향을 주는 범위를 직접 정의하고, 문제를 줄여나가는 것이 최적화라고 생각합니다. 측정 > 분석 > 개선의 플로우가 반복되면 사용자에게 최고의 UX 선사할 수 있는 서비스가 완성될 것이라 생각합니다 😎


Profile picture

주희(Joy)
가치를 고민하는 과정을 함께해요