← /blog # Performance 2026.02.11 14 min

Lighthouse 52점에서 87점까지 — 번들을 반으로 깎는 4단계

측정 → 진단 → 동적 임포트 → 폰트 서브셋. 추측하지 않고 숫자로 푸는 성능 개선의 한 사례.

성능 최적화는 추측이 아니라 측정에서 시작한다. 숫자가 문제를 가리키면, 해결은 의외로 단순하다.

52점짜리 메인 페이지

어웨어랩에서 투자 포트폴리오 분석 멤버십 플랫폼의 프론트엔드를 단독으로 맡고 있었다. 1차 개발이 끝나고 Lighthouse를 돌려봤는데, 메인 페이지 Performance 점수가 52점이었다. LCP는 9~10초, CLS는 0.126. SEO 랭킹을 올려야 하는 플랫폼에서 이 숫자는 치명적이었다.

어디서부터 손대야 할지 감이 안 왔다. 코드를 대충 훑어보며 "이게 무거울 것 같은데" 하고 추측하는 것은 위험하다. 측정 도구부터 붙이기로 했다.

Step 1 — @next/bundle-analyzer로 측정하기

번들 분석기를 Next.js 설정에 붙였다. 설정은 간단하다.

javascript next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
  enabled: process.env.ANALYZE === "true",
});

module.exports = withBundleAnalyzer({
  // ... existing config
});

ANALYZE=true npm run build를 실행하면 브라우저에 트리맵이 열린다. 각 모듈이 번들에서 차지하는 비율을 시각적으로 보여준다. 이 시점에서 초기 번들 크기는 253KB였다.

Step 2 — 범인을 진단하다

트리맵에서 세 가지가 눈에 띄었다.

  • Directus CMS SDK — 메인 페이지에서 쓰지 않는 CMS 관련 모듈이 초기 번들에 포함
  • 차트 라이브러리 — 포트폴리오 대시보드에서만 쓰는 차트 라이브러리가 모든 페이지의 초기 번들에 포함
  • GSAP — 마케팅 애니메이션용인데 모든 라우트에서 로딩

세 가지 모두 같은 패턴이었다. 특정 페이지에서만 쓰는 모듈이 초기 번들에 포함되고 있었다. 일반적인 import 문으로 가져오면 빌드 시 하나의 청크에 묶이기 때문이다.

Step 3 — 동적 import로 코드 스플리팅

Next.js의 next/dynamic으로 해당 컴포넌트들을 동적 import로 전환했다.

typescript page.tsx
import dynamic from 'next/dynamic';

// Before: import PortfolioChart from '@/widgets/PortfolioChart';
// After:
const PortfolioChart = dynamic(
  () => import('@/widgets/PortfolioChart'),
  { ssr: false, loading: () => <ChartSkeleton /> }
);

핵심은 ssr: falseloading 컴포넌트다. 차트나 GSAP 같은 클라이언트 전용 모듈은 서버에서 렌더할 필요가 없고, 로딩 중에 Skeleton UI를 보여주면 CLS도 방지할 수 있다. CMS SDK, 차트 라이브러리, GSAP 세 가지 모두 동적 import로 전환했다.

Step 4 — 폰트 서브셋 생성

bundle-analyzer에는 안 보이지만 네트워크 탭에서 또 다른 범인이 보였다. 웹폰트 파일이 각각 800KB. 4개 weight를 로딩하면 3.2MB다. 폰트가 LCP 요소보다 먼저 로딩되면서 렌더링을 블로킹하고 있었다.

사용하는 글리프만 추출하는 서브셋을 생성했다. 800KB → 250KB/파일로 줄였다. 추가로 폰트 로딩 순서를 변경해서 LCP 요소가 폰트보다 먼저 렌더되게 했다. FOUT(Flash of Unstyled Text)가 발생하지만 CLS 영향은 미미했고, SEO 랭킹 목표에 따라 LCP 가중치를 우선 판단했다.

CLS 0 달성 — Skeleton UI

CLS 0.126의 원인은 동적 컴포넌트가 로딩되면서 레이아웃이 밀리는 것이었다. 동적 import의 loading 속성에 같은 높이의 Skeleton 컴포넌트를 넣어서, 로딩 전후로 레이아웃 시프트가 0이 되게 했다. 이것으로 CLS 0을 달성했다.

결과

  • 번들 사이즈: 253KB → 136KB (46% 감소)
  • LCP: 9~10초 → 3.8초 (약 60% 개선)
  • CLS: 0.126 → 0
  • Lighthouse Performance: 52점 → 87점

교훈: 측정이 먼저다

52점을 87점으로 올리는 데 걸린 시간은 약 1주일이었다. 그 1주일에서 가장 중요한 하루는 번들 분석기를 붙이고 트리맵을 읽는 하루였다. 추측하지 않고 측정하면, 문제가 가리키는 해결책은 의외로 단순하다 — 동적 import로 분리하고, 폰트를 줄이고, Skeleton으로 CLS를 잡으면 된다.

  • 성능 최적화의 첫 단계는 번들 분석기를 붙이는 것이다
  • 초기 번들에 불필요한 모듈이 포함되어 있으면 동적 import로 분리하라
  • 폰트 서브셋은 비용 대비 효과가 가장 큰 최적화 중 하나다
  • CLS는 Skeleton UI로 해결할 수 있다 — 동적 컴포넌트에 같은 높이의 플레이스홀더를 넣어라
  • 추측하지 말고 측정하라. 숫자가 문제를 말해준다