[리팩토링] Advent Calendar 번들 최적화로 모바일 메인 로딩 시간 단축

Melcoding·2026년 4월 25일

1. 리팩토링 계기

> advent-calendar@0.0.0 build
> tsc -b && vite build

vite v7.2.4 building client environment for production...
[baseline-browser-mapping] The data in this module is over two months old.  To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
✓ 1831 modules transformed.
dist/index.html                   0.80 kB │ gzip:   0.40 kB
dist/assets/index-BaVaWQKe.css   38.68 kB │ gzip:   7.54 kB
dist/assets/index-CRfGvi0N.js   791.68 kB │ gzip: 249.92 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 2.35s

vite에서 500kb 이상 경고가 뜨는 것 확인.
데스크톱에서는 로딩이 걸리지 않으나 모바일에서는 오래 걸리는 현상을 확인해서 혹시 해당 부분이 연관되어 있지 않을까? 고민하게 되었음.
특히나 해당 서비스는 사진을 올리는게 주된 사용인 웹이라 실질적으로 사진을 올리기 편한 모바일에서도 사용을 많이 했었음.


2. 원인 분석: 번들크기 확인하여 방안 강구

경고에서 index.js의 크기가 700kB 이상이라고 하니 어떤 부분이 많이 차지 하는지 확인을 위해 번들 분석이 가능한 도구를 설치하여 확인해봄.

번들 분석 도구 설치

npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    react(),
    visualizer({
      open: true,
      filename: 'dist/stats.html',
      gzipSize: true,
      brotliSize: true,
    })
  ],
})

빌드 후 dist/stats.html에서 시각적으로 번들 구성 확인 가능

실제 번들 구성 (770KiB 기준)

0 ~ 245KiB    React + react-router + date-fns + 앱 코드
245 ~ 578KiB  Firebase (auth + firestore + storage)  ← 약 333KiB
578 ~ 759KiB  lucide-react (아이콘)                  ← 추정치 (불확실)
759 ~ 770KiB  Radix UI + 나머지
  • Firebase가 전체 번들의 약 43% 차지
  • gzip 기준 전체 250KiB → Firebase 기여분 약 110~120KiB

3. 해결 방안 강구

그냥 막연히 생각했을 때 번들 크기 자체를 줄이면 되지 않을까? 라는 생각이 먼저 들었다.
그러고 나서 메시지에서 말해준대로 chunk를 찾아 보았다.

1) 번들 자체 크기 줄이기의 결론은 줄일 수 없음

Firebase 줄이기

Firebase의 크기가 커서 줄일 수 있나 확인하니 경량 대체 없고 tree-shaking 정상 작동

Firebase 서비스대안가능 여부
firebase/firestoreFirestore LiteonSnapshot 실시간 리스너 사용 중
firebase/auth경량 대체 없음❌ Google OAuth 리다이렉트 전체 플로우 필요
firebase/storage경량 대체 없음❌ 이미지 업로드 필요
  • 이미 Modular SDK (v9+) 사용 → tree-shaking 정상 작동
  • Compat SDK(firebase/compat/*) 미사용 → 추가 최적화 여지 없음
  • 지금이 Firebase 하드 최솟값

lucide-react — tree-shaking 정상 작동 중

  • 실제 사용 아이콘: ChevronLeft, ChevronRight, UploadCloud, X (4개)
  • sideEffects: false 설정 확인 → tree-shaking 작동
  • 4개 아이콘 기준 실제 번들 기여분: 약 5~10KiB (위 추정치 181KiB는 마커 방식 오차)

파일 삭제 효과 미미

  • 파일이 존재해도 import하는 곳이 없으면 Vite가 번들에서 제외한다.
  • JS 번들 크기를 줄이려면 실제로 import되는 파일의 코드를 줄여야 한다.

2) 실질적 개선 방향은 manualCunks

JS 전체 크기 자체는 건드릴 수 없으므로 첫 화면 로드 시 필요한 것만 로드되도록 분리하여 첫 화면 진입 속도를 높이고 재사용성을 강화시키는 방향

현재:

  • 첫 방문 시 Firebase 333KiB 포함 770KiB 전부 로드

개선:

  • lazy loading → 첫 화면(홈/로그인)만 로드, 나머지 라우트 진입 시 추가 로드, 재방문시 캐시 그대로 사용
  • manualChunks

4. 목표 설정: 효과가 있음을 확인하는 방법은?

아래 두가지를 생각했다.

  • 일단 빌드시 index.js 파일의 크기를 경고가 발생하지 않는 500kB로 줄이기!
  • 두 번째는 실제 사용성에 효과가 있도록 모바일의 로딩 시간이 줄어드는 것 확인하기!

1) 빌드시 index.js 파일의 크기 확인

요건 빌드시에 어떻게 되는지 확인하기!

2) 모바일의 성능 측정: LIghthouse 지표 활용

Lighthouse 성능 측정

얼마만큼 차이가 있는지 등등 목표 설정 및 비교를 위한 성능 측정 툴을 찾다가 Lighthouse를 알게 되었고 해당 툴로 성능을 측정하게 되었음.

참고: Lighthouse 4대 카테고리별 상세 지표 구성

목표

  • index.js 파일 크기 감소
  • Lighthouse Performance 점수 기존보다 증가
  • LCP 기존보다 감소 - 로딩 후 홈화면이 뜨는 시간
  • TTI 기존보다 감소 - 페이지의 interaction이 가능한 시간

5. 해결 방안 적용: 코드 스플리팅 (lazy loading) & manualChunks 적용

적용 내용

  • App.tsx — 4개 페이지 React.lazy() + Suspense 적용
    - 홈화면에 활용하는 HomePage를 제외한 나머지 페이지에 lazy loading 적용함
  • vite.config.tsmanualChunks (vendor / firebase / ui) 설정

각 방법별 기대 효과

1) 코드 스플리팅 (lazy loading) 효과

처음 방문할 때 불필요한 코드를 안 받아 첫 화면 진입시 로딩 시간이 줄어드는 효과

/ 접속 시 받는 것:
  Before: index.js 791KiB (모든 페이지 코드 포함)
  After:  index.js 226KiB + firebase 360KiB + vendor 44KiB + ui 122KiB
          → HomePage에 필요한 것만
  • /projects/:id 처음 진입 시 그 시점에 ProjectDetailPage-19KiB만 추가 로드
  • 안 가는 페이지 코드는 평생 안 받을 수도 있음
  • LCP에 직접 기여 — 첫 화면 렌더링에 필요한 JS가 줄어들어 파싱/실행 시간 단축

2) manualChunks 효과

배포 후 재방문자가 이미 받은 걸 또 안 받아도 됨!
앱을 수정해서 재배포하면:

Before: index.js 해시값 바뀜 → 791KiB 전부 다시 다운로드
After:  index.js 해시값 바뀜 →  72KiB만 다시 다운로드
        firebase / vendor / ui는 캐시 그대로 사용

Firebase 코드는 내가 손대지 않는 한 절대 안 바뀌므로 브라우저 캐시에 영구적으로 남음.

한 줄 요약

누가 이득무엇이 개선
코드 스플리팅첫 방문자초기 로딩 속도 (LCP)
manualChunks재방문자배포 후 캐시 효율

6. 결과

1) 빌드시 index.js 파일 Before / After 비교

총합은 비슷하지만 코드 스플리팅으로 index.js 자체 크기는 줄어듬

상세 내역

Before

index.js            791KiB   gzip: 250KiB

After

firebase.js         360KiB   gzip: 113KiB  ← 배포마다 캐시 재사용
ui.js               122KiB   gzip:  39KiB  ← 배포마다 캐시 재사용
index.js            226KiB   gzip:  72KiB  ← 앱 코드만
vendor.js            44KiB   gzip:  16KiB  ← 배포마다 캐시 재사용
ProjectDetailPage    19KiB   gzip:   6KiB  ← 라우트 진입 시 로드
CreateProjectPage    11KiB   gzip:   4KiB  ← 라우트 진입 시 로드
JoinProjectPage       4KiB   gzip:   1KiB  ← 라우트 진입 시 로드
ProjectListPage       3KiB   gzip:   1KiB  ← 라우트 진입 시 로드

실제 빌드 메시지 비교

Before

> advent-calendar@0.0.0 build
> tsc -b && vite build

vite v7.2.4 building client environment for production...
[baseline-browser-mapping] The data in this module is over two months old.  To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
✓ 1831 modules transformed.
dist/index.html                   0.80 kB │ gzip:   0.40 kB
dist/assets/index-BaVaWQKe.css   38.68 kB │ gzip:   7.54 kB
dist/assets/index-CRfGvi0N.js   791.68 kB │ gzip: 249.92 kB

(!) Some chunks are larger than 500 kB after minification. Consider:
- Using dynamic import() to code-split the application
- Use build.rollupOptions.output.manualChunks to improve chunking: https://rollupjs.org/configuration-options/#output-manualchunks
- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.
✓ built in 2.35s

After

> advent-calendar@0.0.0 build
> tsc -b && vite build

vite v7.2.4 building client environment for production...
[baseline-browser-mapping] The data in this module is over two months old.  To ensure accurate Baseline data, please update: `npm i baseline-browser-mapping@latest -D`
✓ 1832 modules transformed.
dist/index.html                              1.03 kB │ gzip:   0.46 kB
dist/assets/index-Bv0GhR_x.css              28.01 kB │ gzip:   5.95 kB
dist/assets/ProjectListPage-B4mZjvUp.js      3.02 kB │ gzip:   1.34 kB
dist/assets/projects-D28OFEBP.js             3.54 kB │ gzip:   2.02 kB
dist/assets/JoinProjectPage-e3c2VJBW.js      3.56 kB │ gzip:   1.30 kB
dist/assets/CreateProjectPage-scnHJhex.js   10.72 kB │ gzip:   3.62 kB
dist/assets/ProjectDetailPage-BL0Rro_F.js   18.57 kB │ gzip:   6.15 kB
dist/assets/index-BdTa1noy.js               19.76 kB │ gzip:   7.39 kB
dist/assets/ui-BEkj4lTN.js                 147.39 kB │ gzip:  46.75 kB
dist/assets/vendor-D1TdEGAq.js             225.68 kB │ gzip:  72.35 kB
dist/assets/firebase-CetztI6f.js           360.45 kB │ gzip: 112.51 kB
✓ built in 3.67s

2) Lighthouse 성능 비교

⭕️Lighthouse Performance 점수 비교

데스크탑의 점수는 3점 낮아졌으나 90점대로 유지이고, 걱정되었던 모바일의 성능 점수가 7점 상승했음

모바일
페이지BeforeAfter변화
메인7683+7
데스크탑
페이지BeforeAfter변화
메인9996-3

⭕️LCP 비교: 페이지 내 가장 큰 이미지/텍스트 요소가 렌더링되는 시간

모바일의 LCP 시간이 0.6초 감소하여 렌더링 시간 조금 단축되어 즉, 홈화면이 조금 빨리 뜨게 됨

모바일 - LCP
페이지BeforeAfter변화
메인5.1s4.5s-0.6s
데스크탑 - LCP
페이지BeforeAfter변화
메인0.8s0.8s-

❌TTI 비교: 페이지가 완전히 상호작용 가능한 상태가 되는 시간

TTI의 시간은 오히려 0.2초 증가함! 다만 LCP에서 0.6초 감소하여 전반적으로는 홈화면이 뜨고 상호작용까지 0.4초 증가

모바일 - TTI
페이지BeforeAfter변화
메인5.1s5.3s+0.2s
데스크탑 - TTI
페이지BeforeAfter변화
메인0.8s0.8s-

7. 목표 달성 여부

  • ⭕️ index.js 파일 크기 감소
  • ⭕️ Lighthouse Performance 점수 기존보다 증가
  • ⭕️ LCP 기존보다 감소 - 로딩 후 홈화면이 뜨는 시간
  • ❌ TTI 기존보다 감소 - 페이지의 interaction이 가능한 시간

8. 느낀 점

프론트엔드의 성능을 어떻게 측정하고 증명하는지에 대해 관심이 많았었는데 이번 기회에 문제를 정의하고 어떻게 증명해내는지 직접 해보면서 경험치를 쌓을 수 있었다.
문제의 원인을 예측하고 예측이 맞는지 확인하는 과정에서 몰랐던 여러 지표와 내용들을 부딪히면서 알아가는 과정에서 많은 것을 고민하고 배웠다.
다만, 아쉬운 점은 내가 설정한 목표 지표가 타당한 목표인지에 대해서는 좀 더 서칭하고 공부해야할 것 같다.
이후에는 모바일의 performance 점수가 90점 이상으로 획기적으로 오르지 않아서 해당 점수를 더 올릴 방안을 찾아봐야하고 더불어 SEO 점수도 높일 수 있도록 추가적인 리팩토링 작업이 필요할 것 같다.
이외에도 다양한 성능 측정 내용이 있는데 해당 내용을 하나씩 파면서 측정하여 비교하여 성능을 높이는 여러가지 방안을 생각해보고 높이면서 문제 해결 역량을 차근차근 높일 예정!

관련 깃허브 레포지토리: https://github.com/meldyssey/advent-calendar/pull/6


참고자료

0개의 댓글