웹폰트 하나 바꿨을 뿐인데 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로 원샷 대시보드 생성 에이전트를 만들어 민관협력 공모전에서 특별상을 수상한 이야기