treeru.com
개발

Next.js 사이트 PageSpeed 최적화 — Mobile 38에서 88까지

2026-01-14
Treeru

솔직히 말하면, PageSpeed 점수를 처음 측정했을 때 충격을 받았습니다. 디자인도 신경 썼고, SEO도 100점인데 Mobile Performance가 38점이었습니다. Desktop도 52점. 사이트가 예쁘다고 빠른 건 아니라는 걸 숫자로 확인한 순간이었습니다.

이 글은 Next.js 기반 사이트의 PageSpeed 점수를 Mobile 38 → 88, Desktop 52 → 97까지 끌어올린 과정을 정리한 것입니다. 화려한 이론이 아니라, 실제로 뭘 바꿨고 몇 점이 올랐는지를 기록했습니다.

노트북 화면에 PageSpeed Insights 성능 점수가 표시된 웹 개발 작업 환경 — Mobile 38점에서 88점으로 개선한 과정
PageSpeed 측정 — Next.js 사이트의 Core Web Vitals 개선 전후 점수 비교

38→88

Mobile 점수

52→97

Desktop 점수

0.5→0

CLS 개선

110초→3.4초

LCP 개선

1상황 — Mobile 38점

사이트의 기능과 디자인은 완성된 상태였습니다. SEO 100점, Accessibility 92점, Best Practices 100점. 문제는 Performance 하나였습니다. 측정 결과를 보면 원인이 뚜렷했습니다.

초기 Core Web Vitals (Mobile)

  • LCP (Largest Contentful Paint): 110.2초 — 목표 2.5초 이하
  • CLS (Cumulative Layout Shift): 0.500 — 목표 0.1 이하
  • FCP (First Contentful Paint): 3.3초 — 목표 1.8초 이하
  • Speed Index: 46.4초

LCP 110초. 오타가 아닙니다. Hero 이미지 하나가 5.6MB였고, 비슷한 크기의 이미지가 6장 더 있었습니다. 총 36MB의 이미지를 슬로우 4G 환경에서 로드하니 110초가 걸린 겁니다.

CLS 0.5도 심각했습니다. 타이핑 애니메이션이 글자를 하나씩 추가하면서 레이아웃을 계속 밀어냈고, 웹폰트가 뒤늦게 로드되면서 텍스트 크기가 바뀌었습니다.

24단계 개선 요약

한 번에 모든 걸 바꾼 게 아니라, 한 가지씩 수정하고 측정하기를 반복했습니다. 덕분에 각 단계에서 얼마나 효과가 있었는지 정확히 파악할 수 있었습니다.

단계MobileDesktopCLSLCP
초기 상태38520.500110.2초
이미지 압축45800.4816.8초
CLS 수정70880.0954.7초
폰트 최적화749604.7초
LCP 최적화889703.4초

핵심 포인트

가장 큰 점수 상승은 CLS 수정 단계(+25점)에서 발생했습니다. 하지만 가장 먼저 해야 할 작업은 이미지 압축입니다. 이미지가 무거우면 다른 최적화의 효과가 묻혀버리기 때문입니다.

3Phase 1: 이미지 압축

Mobile 38 → 45 (+7) · Desktop 52 → 80 (+28)

모든 걸 제쳐두고 이미지부터 손봤습니다. Hero 이미지 1장이 5.6MB, 서비스 섹션 이미지 3장이 각각 5~7MB, 배경 텍스처와 사진까지 합치면 총 6장, 36MB였습니다.

Sharp 라이브러리로 WebP 이미지 압축 작업 중인 개발자 데스크 — 36MB를 0.8MB로 줄인 이미지 최적화 환경
이미지 압축 Phase 1 — sharp로 WebP 변환 후 36MB → 0.8MB, LCP 110초 → 6.8초 개선
이미지변경 전변경 후감소율
Hero 메인5,695KB124KB97.8%
서비스 (AI)5,406KB93KB98.3%
서비스 (네트워크)6,789KB214KB96.8%
서비스 (프로그램)6,082KB155KB97.5%
배경 텍스처6,170KB122KB98.0%
배경 사진6,282KB97KB98.5%

Node.js의 sharp 라이브러리로 WebP quality 80, 최대 너비 1920px로 리사이즈했습니다. 총 36MB가 0.8MB로 줄었습니다. 이것만으로 LCP가 110초에서 6.8초로 내려왔습니다.

왜 Mobile은 7점만 올랐나?

Desktop은 28점이나 올랐는데 Mobile은 7점뿐입니다. 이미지 외에 CLS 0.481이라는 큰 감점 요소가 남아있었기 때문입니다. Lighthouse는 CLS에 전체 점수의 25%를 배정합니다.

4Phase 2: CLS 제거

Mobile 45 → 70 (+25) · CLS 0.481 → 0.095

이 단계에서 가장 큰 점수 상승이 있었습니다. CLS(Cumulative Layout Shift)는 페이지 로딩 중 요소가 밀리는 정도를 측정하는 지표인데, 원인은 의외의 곳에 있었습니다.

범인: 타이핑 애니메이션

메인 페이지에 글자가 한 자씩 나타나는 타이핑 효과가 있었습니다. 시각적으로는 멋졌지만, 글자가 추가될 때마다 텍스트의 높이와 너비가 바뀌면서 아래 콘텐츠를 밀어냈습니다. 이 컴포넌트가 4개 섹션에서 사용되고 있었고, 전체 CLS의 60~70%를 차지하고 있었습니다.

해결: Invisible Placeholder 패턴

핵심 아이디어는 간단합니다. 전체 텍스트를 처음부터 렌더링하되, 아직 타이핑되지 않은 부분은 visibility: hidden으로 숨기는 것입니다.

// 핵심 로직 (단순화)

// 타이핑된 부분 — 보임
<span>{text.slice(0, typedCount)}</span>

// 아직 안 타이핑된 부분 — 공간만 차지
<span style={{ visibility: "hidden" }}>
  {text.slice(typedCount)}
</span>

이렇게 하면 전체 텍스트의 공간이 처음부터 확보되어 레이아웃이 고정됩니다. 사용자 눈에는 똑같이 한 글자씩 나타나지만, 브라우저 입장에서는 레이아웃 변화가 없습니다.

추가 수정: Header Suspense

Header 컴포넌트를 Suspense로 감싸면서 fallback으로 80px 높이의 빈 div를 넣어둔 게 있었습니다. 그런데 Header가 position: fixed라서 document flow에 참여하지 않습니다. 즉, fallback의 80px 공간이 Header 로드 후 사라지면서 CLS가 발생했습니다. fallback을 null로 바꿔서 해결했습니다.

CLS에 포함되지 않는 것들

transform: translateY() 기반 애니메이션, opacity 변경, fixed/absolute 포지션 요소의 변화는 CLS에 포함되지 않습니다. Framer Motion의 fadeInUp 애니메이션(y: 60 → 0)은 transform이므로 CLS 원인이 아니었습니다.

5Phase 3: 폰트 최적화

Mobile 70 → 74 (+4) · Desktop 88 → 96 (+8) · CLS 0.095 → 0

점수 변화만 보면 작아 보이지만, 이 단계에서 CLS가 완전히 0이 되었습니다. 남은 CLS 0.095는 웹폰트 로드 시 텍스트 크기가 바뀌면서 발생한 것이었습니다.

한국어 웹폰트 서브셋 최적화 작업 — Noto Serif KR @font-face 124개에서 1개로 줄인 개발 환경
Phase 3 폰트 최적화 — 커스텀 서브셋으로 렌더 차단 CSS 26KB → 3KB 감소

문제: 한국어 폰트의 @font-face 폭발

next/font/google로 Noto Serif KR을 로드하면, 한글 유니코드 범위별로 120개 이상의 @font-face 규칙이 생성됩니다. 이것만으로 렌더 차단 CSS가 약 26KB 추가됩니다. 영문 폰트에서는 상상하기 어려운 규모입니다.

해결: 커스텀 서브셋 + local font

실제 사이트에서 사용하는 한글 문자만 추출하고, Google Fonts의 text API를 통해 해당 글자만 포함된 woff2 파일을 생성했습니다. 그 다음 next/font/local로 로드하니 @font-face 규칙이 1개로 줄었고, 렌더 차단 CSS가 26KB에서 3KB로 감소했습니다.

Before

124개

@font-face 규칙

~26KB

렌더 차단 CSS

After

1개

@font-face 규칙

~3KB

렌더 차단 CSS

font-display: optional

font-display: swap은 폴백 폰트를 먼저 보여주고 웹폰트가 로드되면 교체합니다. 이 교체 시점에 텍스트 크기가 바뀌면 CLS가 발생하고, LCP도 재측정됩니다. optional로 바꾸면 100ms 내에 로드되지 않으면 폴백을 유지합니다. 일반 네트워크에서는 100ms 안에 로드되니 실제로는 웹폰트가 표시되지만, 느린 환경에서의 폰트 스왑과 CLS를 방지합니다.

실험: Google Fonts CDN 직접 로드

“그러면 Google CDN에서 바로 불러오면 더 빠르지 않을까?” 테스트해봤습니다. 결과는 Mobile 80 → 69로 11점 하락. 외부 CDN으로의 DNS + TLS 핸드셰이크 오버헤드가 슬로우 4G에서 200~400ms를 추가했습니다. 항상 self-host가 답입니다.

6Phase 4: LCP 개선

Mobile 74 → 88 (+14) · Desktop 96 → 97 (+1) · LCP 4.7초 → 3.4초

마지막 단계는 LCP(Largest Contentful Paint)를 3.4초까지 줄이는 것이었습니다. 여기서 가장 큰 함정을 발견했습니다.

함정: motion/react의 opacity:0

Hero 섹션에 Framer Motion으로 페이드인 애니메이션을 넣어둔 상태였습니다. initial={{ opacity: 0 }}으로 시작해서 animate={{ opacity: 1 }}로 나타나는 패턴입니다.

문제는 SSR(Server Side Rendering)에서 이것이 style="opacity:0"으로 렌더링된다는 것입니다. 브라우저는 opacity:0인 요소를 LCP 후보에서 제외합니다. JavaScript가 실행되어 opacity가 1이 된 후에야 LCP가 측정되기 때문에, LCP = JavaScript 로드 + 실행 시간이 되어버립니다. 이 현상의 상세 분석은 LCP opacity:0 트랩 분석을 참고하세요.

❌ LCP 지연

<motion.div
  initial={{ opacity: 0, scale: 0.95 }}
  animate={{ opacity: 1, scale: 1 }}
>
  <img src="/hero.avif"
       fetchPriority="high" />
</motion.div>

✅ LCP 즉시

<motion.div
  initial={{ scale: 0.97 }}
  animate={{ scale: 1 }}
>
  <img src="/hero.avif"
       fetchPriority="high" />
</motion.div>

추가 개선: AVIF 변환 + Preload 정리

Hero 이미지를 WebP에서 AVIF로 변환하여 추가 압축했습니다. 그리고 아래 fold 이미지들에 loading="lazy"를 누락한 곳이 있었는데, Next.js는 lazy가 없는 img 태그에 자동으로 preload를 생성합니다. 이 불필요한 preload들이 Hero 이미지와 대역폭을 경쟁하고 있었습니다.

LCP 최적화 원칙

LCP 후보(Hero 이미지, 대형 Heading)를 감싸는 요소에는 opacity: 0 초기값을 사용하지 않습니다. preload는 LCP 이미지 1개에만 적용하고, 나머지 이미지에는 반드시 loading="lazy"를 붙입니다.

7핵심 교훈

이미지부터 시작하세요

가장 쉽고 효과가 큽니다. sharp로 WebP/AVIF 변환 + 리사이즈만 해도 Performance 점수가 눈에 띄게 올라갑니다.

애니메이션은 CLS를 유발할 수 있습니다

타이핑 효과, 높이가 변하는 아코디언, 지연 로드되는 콘텐츠 — 레이아웃을 바꾸는 모든 것이 CLS의 원인입니다. transform과 opacity는 안전합니다.

한국어 폰트는 특별히 신경 써야 합니다

영문 폰트와 달리 120개 이상의 서브셋이 생성될 수 있습니다. 커스텀 서브셋 + local font + font-display: optional이 최적 조합입니다.

motion 라이브러리의 opacity:0은 LCP를 망칩니다

SSR에서 opacity:0이 인라인 스타일로 렌더링되어 LCP 후보에서 제외됩니다. Hero 이미지 영역에는 opacity 대신 scale이나 transform만 사용하세요.

하면 안 되는 것

시도결과
Google Fonts CDN 비동기 로딩11점 하락 — 외부 DNS/TLS 오버헤드
아래 fold 이미지에 preload 추가LCP 악화 — Hero와 대역폭 경쟁
빌드 캐시 재사용 (rm -rf .next 없이)변경 사항 미반영
experimental: { optimizeCss: true }Next.js 16 Turbopack에서 효과 없음

이 글의 핵심 정리

  • 이미지 압축이 첫 번째 — 36MB를 0.8MB로 줄여 LCP 110초 → 6.8초
  • CLS 수정이 가장 큰 점수 향상 — 타이핑 애니메이션의 Invisible Placeholder 패턴으로 +25점
  • 한국어 폰트는 커스텀 서브셋 필수 — @font-face 124개 → 1개, CSS 26KB → 3KB
  • LCP 후보 요소에 opacity:0 금지 — Framer Motion 사용 시 scale만 적용
  • 측정 → 수정 → 재측정 사이클이 핵심 — 한 번에 모든 걸 바꾸지 마세요

각 최적화 단계에서 Core Web Vitals 디버깅을 활용하면 원인을 정확히 파악할 수 있습니다.

본 데이터는 2026년 2월에 측정되었습니다. PageSpeed 점수는 네트워크 상태, 서버 응답 시간 등에 따라 ±3~5점 변동될 수 있습니다. Lighthouse 점수 가중치: LCP 25%, TBT 30%, CLS 25%, FCP 10%, SI 10%. 본 콘텐츠의 비상업적 공유는 자유이나, 상업적 이용 시 문의 페이지를 통해 연락 바랍니다.

웹사이트 성능 최적화가 필요하신가요?

Treeru가 PageSpeed 분석부터 최적화 구현까지, 웹사이트 성능을 개선해 드립니다.

무료 상담 신청하기
T

Treeru

웹 개발, IT 인프라, AI 솔루션 분야의 실무 인사이트를 공유합니다. 기업의 디지털 전환을 돕는 IT 파트너, Treeru입니다.

공유

댓글

(4개)
4.63/ 5

로그인하면 댓글을 작성할 수 있습니다.

2026-01-28
4.554.5

단계별로 점수 변화를 보여주니까 어디서 얼마나 효과가 있는지 바로 감이 옵니다. 이미지부터 시작하라는 조언 감사합니다.

2026-01-25
454.0

한국어 웹폰트가 렌더 차단 CSS를 26KB나 만든다는 부분이 공감됩니다. 한국어 사이트는 항상 성능이 떨어지는 이유가 있었네요. 서브셋 방법 유용합니다.

2026-01-21
555.0

motion/react의 opacity:0이 LCP를 망친다는 건 처음 알았습니다. 저희 사이트도 Framer Motion 쓰는데 한번 확인해봐야겠네요. 실측 데이터가 있어서 설득력 있습니다.

관련 글

© 2026 TreeRU. All rights reserved.

본 콘텐츠의 저작권은 TreeRU에 있으며, 출처를 밝히지 않은 무단 전재 및 재배포를 금합니다. 인용 시 출처(treeru.com)를 반드시 명시해 주세요.