웹폰트 하나 바꿨을 뿐인데 LCP가 73% 개선됐다
Pretendard dynamic-subset의 함정과 시스템 폰트 전환으로 LCP 4.1초 → 1.1초 개선한 경험입니다.
문제: LCP 4초, 뭘 해도 안 줄었다
포트폴리오 사이트의 PageSpeed Performance 점수가 75점이었습니다. 모바일 LCP(Largest Contentful Paint)가 4.1초로, “Poor” 판정을 받고 있었습니다.
이미지는 이미 WebP로 변환하고, lazy loading도 적용했습니다. 빌드 시 자동 압축도 걸려 있었습니다. 그런데도 LCP는 꿈쩍도 안 했습니다.
“GitHub Pages라서 느린 건가?” 싶었지만, TTFB(서버 응답 시간)는 13ms로 빨랐습니다. 서버 문제가 아니었습니다.
원인 추적: PageSpeed API로 하나씩 확인
PageSpeed Insights의 요약 점수만 보면 원인을 알 수 없습니다. API로 상세 데이터를 뽑아서 하나씩 확인했습니다.
| 의심 원인 | 측정값 | 판정 |
|---|---|---|
| 서버 응답 (TTFB) | 13ms | 정상 |
| 렌더링 차단 리소스 | 없음 | 정상 |
| 이미지 크기 | 최적화됨 | 정상 |
| 폰트 HTTP 요청 | 14개 | 원인 |
폰트 파일이 14개나 로드되고 있었습니다.
”14개면 많은 건가?”
네트워크 요청 목록을 보니 Pretendard 폰트의 subset 파일들이었습니다:
PretendardVariable.subset.78.woff2 (26KB)PretendardVariable.subset.81.woff2 (26KB)PretendardVariable.subset.82.woff2 (26KB)PretendardVariable.subset.83.woff2 (27KB)PretendardVariable.subset.84.woff2 (24KB)... (총 14개, 363KB)363KB면 이미지 한 장보다 작습니다. 용량이 문제가 아니었습니다.
문제는 요청 수입니다. HTTP 요청은 각각 네트워크 왕복(RTT)이 필요합니다. 빠른 Wi-Fi에서는 RTT가 20-50ms지만, Lighthouse가 시뮬레이션하는 느린 모바일(Slow 4G)에서는 RTT가 150ms 이상입니다.
14개 요청 × 150ms RTT = 2초+ 지연(실제로는 병렬 처리되지만, 브라우저 동시 연결 제한으로 완전 병렬은 아님)dynamic-subset이 뭔데?
Pretendard의 dynamic-subset은 영리한 최적화 방식입니다.
한글 완성형은 “가”부터 “힣”까지 11,172자입니다. 전체를 하나의 폰트 파일로 만들면 2MB가 넘습니다. 그래서 유니코드 범위별로 90개 파일로 쪼개고, CSS의 unicode-range를 사용해 페이지에서 실제로 쓰는 글자가 포함된 파일만 로드합니다.
/* Pretendard dynamic-subset CSS (간략화) */@font-face { font-family: "Pretendard Variable"; src: url("PretendardVariable.subset.78.woff2") format("woff2"); unicode-range: U+AC00- U+B0FF; /* "가"~"뇿" 범위 */}
@font-face { font-family: "Pretendard Variable"; src: url("PretendardVariable.subset.83.woff2") format("woff2"); unicode-range: U+C0AC- U+C8FF; /* "사"~"쿿" 범위 */}/* ... 90개 범위 */랜딩 페이지처럼 텍스트가 적으면 2-3개 subset만 로드됩니다. 2MB → 50KB로 줄일 수 있습니다.
블로그에서는 왜 문제인가
블로그나 포트폴리오는 다릅니다. 경력 설명, 프로젝트 소개, 기술 스택… 다양한 한글을 씁니다. 90개 범위 중 상당수에 해당하는 글자가 등장합니다.
제 홈페이지의 경우:
- 90개 subset 중 14개가 로드됨
- 용량은 363KB로 적지만
- HTTP 요청 14개 = 느린 네트워크에서 병목
dynamic-subset은 텍스트가 적어서 일부 범위만 쓸 때 효과적입니다.
블로그는 거의 모든 범위를 쓰게 되므로 subset이 의미가 없습니다.
해결: 시스템 폰트로 전환
시스템 폰트는 이미 사용자 기기에 설치되어 있습니다. 다운로드가 필요 없습니다.
대안 비교
| 방법 | 장점 | 단점 |
|---|---|---|
| 시스템 폰트 | 다운로드 0, 즉시 렌더링 | OS별로 다른 폰트 |
| 폰트 서브셋 직접 생성 | 필요한 글자만 포함 | 빌드 복잡도 증가, 유지보수 필요 |
| 전체 폰트 단일 파일 | 요청 1개 | 한글 전체 포함 시 2MB+ |
포트폴리오 사이트라 브랜딩보다 속도가 우선이었습니다. 그리고 macOS의 Apple SD Gothic Neo가 Pretendard와 시각적으로 거의 비슷합니다. 시스템 폰트를 선택했습니다.
코드 변경
Layout.astro:
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin /><link rel="preload" href="...pretendardvariable-dynamic-subset.min.css" as="style" /><link rel="stylesheet" href="...pretendardvariable-dynamic-subset.min.css" />global.css:
font-family: 'Pretendard Variable', Pretendard, -apple-system, ...;font-family: -apple-system, BlinkMacSystemFont, 'Apple SD Gothic Neo', 'Malgun Gothic', 'Segoe UI', Roboto, sans-serif;시각적 차이
거의 없습니다.
| OS | 적용 폰트 | Pretendard와 비교 |
|---|---|---|
| macOS/iOS | Apple SD Gothic Neo | 매우 비슷 |
| Windows | Malgun Gothic (맑은 고딕) | 약간 다르지만 가독성 동일 |
| Android | Roboto + 기본 한글 | 약간 다름 |
브랜딩이 중요한 서비스라면 문제겠지만, 기술 블로그에서는 체감되지 않습니다.
결과
| 지표 | 변경 전 | 변경 후 | 개선 |
|---|---|---|---|
| Performance Score | 75점 | 100점 | +25점 |
| LCP | 4.1초 | 1.1초 | -73% |
| FCP | 3.6초 | 1.0초 | -72% |
| 폰트 HTTP 요청 | 14개 | 0개 | -14개 |
| 전체 요청 수 | 62개 | 45개 | -17개 |
정리: 언제 뭘 쓸까
| 상황 | 권장 방식 |
|---|---|
| 랜딩 페이지, 텍스트 적음 | dynamic-subset (2-3개만 로드) |
| 브랜딩 중요, 폰트 일관성 필수 | 전체 폰트 또는 직접 서브셋 |
| 블로그, 포트폴리오, 텍스트 많음 | 시스템 폰트 |
LCP 느릴 때 체크 순서
- 이미지의 크기, 포맷(WebP), lazy loading
- 폰트의 요청 수와 로딩 방식 (→ 이번 케이스)
- JS 번들의 크기, code splitting
- 서버 응답의 TTFB
PageSpeed 요약 점수만 보지 말고, Network 요청 목록을 확인하세요. 예상 못한 곳에서 병목이 생깁니다.
관련 콘텐츠
Claude Code로 포트폴리오와 기술 블로그 운영하기
AI 코딩 에이전트와 협업해 포트폴리오와 기술 블로그를 구축한 경험을 공유합니다. 효과적인 협업 방식, 자동화 시스템 구현, 운영 워크플로우까지 다룹니다.
productivityClaude Code 스킬로 블로그 트래픽 리포트 자동화하기
GA 대시보드 들여다보는 대신 /ga-report 한 줄로 트래픽과 성능을 확인합니다. Google Analytics 연동부터 스킬 구현, 실제 성능 개선 사례까지 공유합니다.
developmentVibeCraft: 민관협력 공모전 특별상 수상기
Gemini CLI로 원샷 대시보드 생성 에이전트를 만들어 민관협력 공모전에서 특별상을 수상한 이야기입니다.