지난 분기 랜딩 페이지 리뉴얼 직후 모바일 LCP p75가 2.1초에서 2.9초로 올라갔다. 원인은 폰트 자체가 아니라 '모든 굵기를 preload'한 설정과 Google Fonts 외부 도메인 연결 비용이 겹친 것이었다. preload 대상을 1개로 줄이고 자체 호스팅으로 옮긴 뒤 p75 LCP는 2.3초로 돌아왔다.

랜딩 페이지 LCP가 2.9초를 넘은 진짜 원인은 폰트였나?

결론부터 말하면 폰트가 유일한 원인은 아니었지만, 폰트 로딩 설정이 이미지 LCP 요소의 요청 시작 시각을 늦춘 핵심 변수였다. 리뉴얼 전에는 제목 텍스트가 LCP 요소였고, 리뉴얼 후 hero 이미지로 LCP 요소가 바뀌었는데도 폰트 전략은 텍스트 LCP 시절 그대로 남아 있었던 게 발단이었다.

  • 텍스트가 LCP 요소인 페이지는 폰트 전략을 이미지 전략과 함께 측정해야 한다. 2025 Web Almanac 인용 자료 기준 모바일 페이지의 약 24%에서 텍스트가 LCP 요소다 출처.
  • 초기 렌더 폰트는 1~2개, 초기 전송량 100~150KB 이하, 전체 295KB 미만을 1차 예산으로 두고 RUM으로 조정한다.
  • Google Fonts 사용 사이트군은 미사용 사이트군보다 모바일 LCP 통과율이 9%p 낮게 관측된 사례가 있지만, 이는 인과 실험이 아니라 사이트군 비교다 출처.
  • preload는 '실제 첫 화면 텍스트를 그리는 폰트'에만 걸어야 하고, 굵기·언어별 파일을 전부 걸면 이미지·JS와 대역폭 경쟁이 생긴다.
  • font-display: swap은 FOIT를 없애지만 CLS까지 해결하지 않는다. 폴백-웹폰트 메트릭 차이는 별도로 측정해야 한다.

리뉴얼 당시 상황은 이랬다. 랜딩 페이지에 hero 이미지와 새 로고 폰트를 추가하면서, 기존에 본문용으로 쓰던 Pretendard 정적 폰트 2굵기에 더해 로고용 가변 폰트 1개, 아이콘 폰트 1개가 추가됐다. 배포 직후 Search Console과 자체 RUM 대시보드에서 모바일 LCP p75가 2.1초에서 2.9초로 튀었고, CLS는 0.04에서 0.07로 늘었다. Core Web Vitals 목표인 LCP 2.5초 미만, CLS 0.1 미만 기준으로는 아직 CLS는 통과였지만 LCP는 미달이었다 출처.

폰트 프리로드를 늘렸는데 왜 LCP는 더 느려졌나?

가설은 "폰트가 느리게 로드돼서 텍스트 렌더가 늦어졌다"였는데, 실제로는 거꾸로였다. 리뉴얼 팀이 "폰트 깜빡임을 없애자"는 의도로 Pretendard 2굵기, 로고 가변 폰트, 아이콘 폰트까지 총 4개 파일을 모두 <link rel="preload">로 걸어뒀다. 이 설정은 리뉴얼 전 텍스트 LCP 시절에는 문제가 없었지만, hero 이미지가 LCP 요소로 바뀐 뒤에는 브라우저의 초기 네트워크 우선순위 큐에서 4개 폰트 요청이 hero 이미지 요청보다 먼저 자리를 차지했다.

Chrome DevTools 워터폴을 열어보니 hero 이미지 요청이 폰트 4개 요청 뒤로 밀려 시작 시각이 약 300ms 늦어진 구간이 보였다. 이미지가 LCP인 페이지에서도 폰트가 초기 대역폭을 점유해 이미지 요청을 늦출 수 있다는 점을, preload를 걸 때는 전혀 고려하지 않았던 것이다 출처. 아이콘 폰트는 사실 첫 화면에 보이지도 않는 하단 푸터 아이콘용이었는데도 preload에 포함돼 있었다.

구글 폰트 CDN이 병목이었나, 우리 쪽 설정이 문제였나?

둘 다였다. 로고용 가변 폰트는 Google Fonts CDN에서 서빙하고 있었는데, 이 도메인에 대한 DNS 조회와 TLS 연결 비용이 별도로 발생했다. 동일 오리진 WOFF2로 자체 호스팅하면 이 연결 비용을 줄일 수 있다는 가이드를 뒤늦게 확인했다 출처. 다만 자체 호스팅으로 옮긴다고 연결 비용만 사라지는 게 아니라, 캐시 정책·CDN 위치·crossorigin 속성까지 같이 맞춰야 효과가 나온다는 걸 이번에 직접 확인했다.

파일 크기도 다시 봤다. 로고 가변 폰트는 OTF 원본 168KB를 그대로 변환 없이 쓰고 있었다. 동일 글꼴 비교 예시에서 OTF 168KB, WOFF 90KB, WOFF2 53KB로 WOFF2가 OTF보다 약 68% 작다는 수치가 있는데 출처, 우리 환경에서는 Brotli 압축과 한글 서브셋을 적용한 뒤 실제 전송량이 61KB로 측정됐다. 수치 자체는 변환 조건마다 다르므로 빌드 후 실제 전송 크기를 다시 재는 게 맞았다.

워터폴로 원인을 어떻게 확정했나?

재현 조건을 고정한 뒤 preload를 하나씩 빼면서 워터폴과 LCP를 같이 기록했다. Chrome 안정 버전, Moto G Power급 CPU 에뮬레이션, Slow 4G, cold cache 조건으로 Lighthouse 랩 측정과 자체 RUM을 병행했다. 4개 preload를 모두 유지한 상태의 LCP는 약 2.8초, 아이콘 폰트 preload만 제거했을 때 2.6초, 추가로 로고 가변 폰트 preload까지 제거하고 본문 폰트 1개만 preload로 남겼을 때 2.2초로 줄었다.

조건 preload 대상 LCP(랩, 근사치)
리뉴얼 직후 본문2+로고+아이콘 약 2.8초
1차 수정 본문2+로고 약 2.6초
2차 수정 본문1 약 2.2초

DebugBear 인용 사례에서 올바른 preload가 LCP를 1.82초에서 1.24초로, 약 32% 줄인 경우가 있다는데 출처, 이는 특정 사이트·환경 사례라 수치를 그대로 우리 사이트에 대입하긴 어려웠다. 다만 방향성, 즉 "불필요한 preload를 줄이면 hero 이미지 요청이 앞당겨진다"는 패턴은 우리 워터폴에서도 그대로 재현됐다.

무엇을 바꾸고 어떤 지표로 확인했나?

세 가지를 바꿨다. 첫째, preload 대상을 본문 폰트 1개(Pretendard Regular, 한글 서브셋 61KB)로 줄이고 로고·아이콘 폰트는 일반 @font-face로만 선언했다. 둘째, 로고 가변 폰트를 자체 오리진 WOFF2로 옮기고 font-weight 축을 실제 쓰는 600·700으로 제한했다. 셋째, 본문 폰트에는 font-display: swap과 size-adjust를 같이 적용해 폴백 글꼴과의 폭 차이를 줄였다.

배포 후 1주일 RUM 기준 모바일 LCP p75는 2.3초, CLS p75는 0.05로 돌아왔다. INP는 변화가 거의 없어 폰트 변경이 메인 스레드 작업량에는 큰 영향을 주지 않았다는 걸 확인했다. 이후 CI에 초기 폰트 전송량 150KB, preload 폰트 파일 수 2개 이하를 예산으로 걸어, 다음 기능 추가 때 폰트가 조용히 늘어나는 걸 PR 단계에서 걸러내게 했다.

같은 함정을 피하려면 기본값을 어떻게 두나?

기본값은 "LCP 요소가 무엇인지 먼저 확인하고, 그 요소를 그리는 데 필요한 리소스만 preload한다"는 원칙이다. 페이지를 리뉴얼해 LCP 요소가 텍스트에서 이미지로, 또는 그 반대로 바뀌었다면 이전 폰트 전략이 그대로 맞는지부터 다시 확인해야 한다. 2026년 기준으로도 페이지당 폰트 전송량 중앙값이 154KB 수준이라는 점을 고려하면 출처, 새 폰트를 추가할 때마다 "이게 예산 안에 들어오는가"를 습관적으로 되묻는 게 장애를 줄이는 쪽에 가까웠다.

핵심 정리

  • 리뉴얼로 LCP 요소가 텍스트에서 이미지로 바뀌면 기존 폰트 preload 설정이 오히려 hero 이미지 요청을 늦출 수 있다.
  • preload는 첫 화면 실제 텍스트를 그리는 폰트 1~2개로 제한하고, 아이콘·로고 폰트는 일반 로드로 분리한다.
  • 자체 호스팅 전환은 연결 비용을 줄일 수 있지만 캐시 정책·crossorigin·CDN 위치를 함께 맞춰야 효과가 난다.
  • font-display: swap은 FOIT만 줄이며 CLS는 size-adjust 등 메트릭 조정과 별도로 측정해야 한다.
  • CI에 초기 폰트 전송량·preload 파일 수 예산을 걸어, 기능 추가 시 폰트가 조용히 늘어나는 회귀를 막는다.