tech

Next.js 번들 사이즈 줄이기 — bundle-analyzer, dynamic import

배경

엘림 사내 안전진단 웹 툴을 운영하면서, 배포 후 페이지 초기 로딩이 느리다는 피드백이 있었다.
코드를 눈으로 봐선 병목을 찾을 수 없어서, @next/bundle-analyzer로 번들을 직접 들여다봤다.


뭐가 문제였나

트리맵을 펼쳐보니 눈에 띄는 덩어리가 바로 보였다.

  • @fullcalendar — 트리맵에서 가장 큰 블록, 캘린더가 없는 페이지에도 포함
  • konva — 캔버스 라이브러리, 캔버스가 없는 페이지에서도 포함
  • jszip — 6개 파일 최상단에 정적 import, 실제 사용은 다운로드 버튼 클릭 시뿐
  • 탭 컴포넌트들 — 현재 탭이 아닌 것까지 초기 로드 시 한꺼번에 불려오고 있었음

전부 "지금 당장 필요하지 않은데 처음부터 다 불러오고 있는" 케이스였다.


해결 1: FullCalendar — dynamic import

// Before
import Calendar from './Calendar'

// After
const Calendar = dynamic(() => import('./Calendar'), { ssr: false })

FullCalendar는 window에 의존하는 로직이 있어서 ssr: false를 붙였다.


해결 2: 탭 컴포넌트 — dynamic import

// Before
import MachineProjectTabContent from './_components/tabs/MachineProjectTabContent'
import PictureListTabContent from './_components/tabs/PictureListTabContent'

// After
const MachineProjectTabContent = dynamic(() => import('./_components/tabs/MachineProjectTabContent'))
const PictureListTabContent = dynamic(() => import('./_components/tabs/PictureListTabContent'))

탭을 클릭하기 전까지 해당 컴포넌트의 JS를 다운로드하지 않는다.


해결 3: jszip — 함수 내 동적 import

// Before
import JSZip from 'jszip'

// After
async function downloadFiles() {
  const { default: JSZip } = await import('jszip')
  // ...
}

6개 파일 모두 동일한 패턴으로 수정했다.
{ default: JSZip } 구조 분해는 jszip이 CommonJS 모듈이라서 필요하다.


해결 4: 이미지 — loading='lazy'

<img loading='lazy' src={pic.presignedUrl} alt={pic.originalFileName} />

사진이 많은 목록 페이지에서, HTML 속성 하나로 화면에 보이는 이미지만 우선 다운로드하게 됐다.


결과

| 페이지 | 최적화 전 | 최적화 후 | 감소량 | |--------|-----------|-----------|--------| | /machine/[id] | 470 kB | 249 kB | -221 kB (-47%) | | /calendar | 365 kB | 169 kB | -196 kB (-54%) |

기준은 하나였다 — "이 모듈이 페이지 첫 진입 시점에 반드시 필요한가?"
아니라면 dynamic import 후보다.