development Frontend Performance LCP PageSpeed

웹폰트 하나 바꿨을 뿐인데 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/iOSApple SD Gothic Neo매우 비슷
WindowsMalgun Gothic (맑은 고딕)약간 다르지만 가독성 동일
AndroidRoboto + 기본 한글약간 다름

브랜딩이 중요한 서비스라면 문제겠지만, 기술 블로그에서는 체감되지 않습니다.

결과

지표변경 전변경 후개선
Performance Score75점100점+25점
LCP4.1초1.1초-73%
FCP3.6초1.0초-72%
폰트 HTTP 요청14개0개-14개
전체 요청 수62개45개-17개

정리: 언제 뭘 쓸까

상황권장 방식
랜딩 페이지, 텍스트 적음dynamic-subset (2-3개만 로드)
브랜딩 중요, 폰트 일관성 필수전체 폰트 또는 직접 서브셋
블로그, 포트폴리오, 텍스트 많음시스템 폰트

LCP 느릴 때 체크 순서

  1. 이미지: 크기, 포맷(WebP), lazy loading
  2. 폰트: 요청 수, 로딩 방식 (→ 이번 케이스)
  3. JS 번들: 크기, code splitting
  4. 서버 응답: TTFB

PageSpeed 요약 점수만 보지 말고, Network 요청 목록을 확인하세요. 예상 못한 곳에서 병목이 생깁니다.

관련 콘텐츠

댓글