한국어 웹폰트가 PageSpeed를 깎는 이유 — 렌더 차단 CSS 26KB→3KB
영문 사이트에서는 한 번도 겪지 못한 문제가 한국어 사이트에서 발생합니다. next/font/google로 Noto Serif KR을 로드했더니 렌더 차단 CSS가 26KB나 추가되었습니다. 영문 폰트를 같은 방식으로 로드하면 1~2KB입니다.
이 글은 한국어 웹폰트가 왜 성능에 큰 영향을 주는지, 그리고 124개의 @font-face를 1개로 줄여 렌더 차단 CSS를 26KB에서 3KB로 감소시킨 과정을 정리한 것입니다.
124→1
@font-face 규칙
26→3KB
렌더 차단 CSS
0.095→0
CLS 완전 제거
+8점
Desktop 상승
1문제 — 124개의 @font-face
Next.js에서 Google Fonts를 사용하는 표준 방법은 next/font/google입니다. 코드는 간단합니다:
import { Noto_Serif_KR } from "next/font/google";
const notoSerif = Noto_Serif_KR({
subsets: ["latin"],
weight: ["400", "700"],
variable: "--font-noto-serif",
});이 코드가 빌드 시 생성하는 CSS를 열어보면 놀라게 됩니다. 124개의 @font-face 규칙이 들어있습니다. 각각 다른 유니코드 범위(unicode-range)를 지정하고, 각각 다른 woff2 파일을 참조합니다.
// 생성된 CSS (124개 중 일부)
@font-face {
font-family: 'Noto Serif KR';
src: url(/_next/static/media/xxx1.woff2) format('woff2');
unicode-range: U+f9ca-fa0b, U+fa0e, ...;
}
@font-face {
font-family: 'Noto Serif KR';
src: url(/_next/static/media/xxx2.woff2) format('woff2');
unicode-range: U+d723-d728, U+d72a-d72b, ...;
}
/* ... 122개 더 ... */이 CSS 전체가 렌더 차단 리소스입니다. 브라우저는 이 CSS를 전부 파싱할 때까지 페이지를 그리지 않습니다. 26KB의 CSS를 추가로 파싱하면 슬로우 4G에서 수백 밀리초가 더 걸립니다. 렌더 차단이 성능에 미치는 영향을 분석할 때 Core Web Vitals 디버깅이 도움이 됩니다.
2왜 한국어 폰트만 이런가
영문 알파벳은 약 100자입니다. 한글은 11,172자(조합 가능한 모든 음절)입니다. 이 차이가 모든 것을 설명합니다.
| 항목 | 영문 폰트 | 한국어 폰트 |
|---|---|---|
| 글리프 수 | ~100자 | ~11,172자 |
| 전체 폰트 크기 (woff2) | ~20KB | ~2MB |
| 서브셋 분할 | 1~2개 | 60~124개 |
| @font-face 규칙 (weight 2개) | 2~4개 | 120~250개 |
| 렌더 차단 CSS | ~1KB | ~26KB |
Google Fonts는 한글 폰트를 유니코드 범위별로 잘게 쪼개서 제공합니다. 실제로 필요한 글자가 포함된 파일만 다운로드하도록 하기 위해서입니다. 아이디어 자체는 좋지만, @font-face 규칙 자체가 렌더 차단 CSS에 포함되는 것이 문제입니다. 실제로 다운로드하지 않는 서브셋의 규칙도 CSS 파싱 대상입니다.
3해결: 커스텀 서브셋
핵심 아이디어: 사이트에서 실제로 사용하는 한글 문자만 포함된 폰트 파일을 만든다. 11,172자가 아니라 실제로 쓰는 몇십~몇백 자만 있으면 됩니다.
Step 1: 사용 중인 한글 문자 추출
해당 폰트를 사용하는 텍스트에서 한글 문자를 추출합니다. 사이트 코드에서 해당 폰트 클래스가 적용된 영역의 텍스트를 모읍니다.
Step 2: Google Fonts text API로 서브셋 생성
Google Fonts에는 잘 알려지지 않은 기능이 있습니다. text 파라미터를 넘기면 해당 문자만 포함된 폰트를 생성해줍니다.
// Google Fonts text API
// 이 URL로 요청하면 지정한 글자만 포함된 CSS가 반환됨
https://fonts.googleapis.com/css2
?family=Noto+Serif+KR:wght@700
&display=swap
&text=가나다라마바사아자차카타파하...
// 반환된 CSS에는 @font-face 규칙이 "1개"만 있음
// → woff2 URL을 추출하여 다운로드Step 3: next/font/local로 로드
import localFont from "next/font/local";
const notoSerif = localFont({
src: "../public/fonts/NotoSerifKR-Bold.woff2",
weight: "700",
style: "normal",
variable: "--font-noto-serif",
display: "optional", // 핵심!
fallback: ["Georgia", "Times New Roman", "serif"],
});Before: next/font/google
- @font-face 124개
- 렌더 차단 CSS ~26KB
- weight 400+700 모두 포함
After: next/font/local
- @font-face 1개
- 렌더 차단 CSS ~3KB
- 실제 사용하는 weight만
미사용 weight 제거
기존 코드에서는 weight: ["400", "700"]으로 두 가지를 로드하고 있었습니다. 그런데 실제로 사이트에서 font-serif 클래스는 항상 font-bold와 함께 쓰이고 있었습니다. weight 400은 한 번도 사용되지 않았습니다. 700만 남기고 제거했더니 @font-face 규칙이 절반으로 줄었습니다.
4font-display 전략
폰트가 로드되기 전에 어떻게 할 것인가? 이 선택이 CLS와 LCP에 직접적인 영향을 줍니다.
| 값 | 동작 | CLS | LCP |
|---|---|---|---|
| swap | 폴백 표시 → 폰트 로드 후 교체 | 위험 | 재측정 |
| optional | 100ms 내 로드 시 사용, 아니면 폴백 유지 | 안전 | 안전 |
| block | 폰트 로드까지 텍스트 안 보임 (최대 3초) | 안전 | 지연 |
swap의 문제
swap은 가장 많이 쓰이는 설정이지만 두 가지 문제가 있습니다. 첫째, 폴백 폰트(Georgia 등)와 웹폰트(Noto Serif KR)의 글자 크기가 다르면 교체 순간에 텍스트가 리플로우됩니다. 이것이 CLS입니다. 둘째, 텍스트 크기가 바뀌면 브라우저가 LCP를 재측정합니다. FCP는 1.5초인데 폰트 스왑 후 LCP가 3.2초로 늘어나는 경우가 생깁니다.
optional이 답인 이유
optional은 100ms라는 짧은 시간 안에 폰트가 로드되면 사용하고, 아니면 폴백을 그대로 유지합니다. 폰트 스왑이 발생하지 않으므로 CLS도 0이고, LCP 재측정도 없습니다.
“그러면 웹폰트가 안 나타나지 않나?” — 커스텀 서브셋을 self-host하면 같은 서버에서 로드되므로 대부분의 환경에서 100ms 안에 로드됩니다. 실제로 일반 네트워크에서는 웹폰트가 정상 표시됩니다. 극단적으로 느린 환경에서만 폴백 폰트가 유지되는데, 어차피 그런 환경에서는 폰트 스왑이 사용자 경험을 더 해칩니다.
5실험: Google CDN 직접 로드
“Google의 글로벌 CDN이 빠르니까, 거기서 직접 불러오면 self-host보다 빠르지 않을까?” 합리적인 추론입니다. 실험해봤습니다.
// layout.tsx <head>에 추가
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="" />
<link
href="https://fonts.googleapis.com/css2?family=Noto+Serif+KR:wght@700&display=optional"
rel="stylesheet"
/>결과: Mobile 80 → 69 (11점 하락)
preconnect를 넣어도 외부 도메인으로의 DNS 조회 + TLS 핸드셰이크에 200~400ms가 소요됩니다. Lighthouse의 슬로우 4G 시뮬레이션에서 이 오버헤드가 치명적입니다. Google CDN은 글로벌 캐시가 강점이지만, PageSpeed 시뮬레이션에서는 캐시가 적용되지 않습니다.
결론: 항상 self-host
next/font/google든 next/font/local이든, Next.js의 font 시스템은 폰트 파일을 같은 도메인에서 서빙합니다. 외부 CDN은 사용하지 마세요. 같은 서버에서 로드하는 것이 PageSpeed에서 항상 빠릅니다.
6결과
| 지표 | 변경 전 | 변경 후 |
|---|---|---|
| Mobile Performance | 70 | 74 |
| Desktop Performance | 88 | 96 |
| CLS (Mobile) | 0.095 | 0 |
| @font-face 규칙 | 124개 | 1개 |
| 렌더 차단 CSS | ~26KB | ~3KB |
| 폰트 weight | 400 + 700 | 700만 |
Mobile은 4점, Desktop은 8점 상승으로 수치만 보면 작아 보입니다. 하지만 이 단계에서 CLS가 완전히 0이 되었습니다. 이전 글에서 타이핑 애니메이션을 수정해 CLS를 0.095까지 줄였는데, 남은 0.095는 폰트 스왑 때문이었습니다. font-display: optional로 폰트 스왑을 없애자 완전히 해결되었습니다.
Desktop이 88에서 96으로 크게 오른 건 렌더 차단 CSS 감소의 직접적인 효과입니다. Desktop은 네트워크가 빠른 환경을 가정하므로, CSS 파싱 시간 감소가 바로 FCP 개선으로 이어졌습니다.
이 글의 핵심 정리
- ✓한국어 폰트는 영문 대비 60~120배 많은 @font-face 규칙을 생성함
- ✓Google Fonts text API로 실제 사용 글자만 포함된 커스텀 서브셋 생성 가능
- ✓next/font/local + 커스텀 서브셋으로 @font-face 124개 → 1개, CSS 26KB → 3KB
- ✓font-display: optional로 폰트 스왑 방지 → CLS 0 + LCP 재측정 방지
- ✓Google CDN 직접 로드는 오히려 11점 하락 — 항상 self-host가 답
본 데이터는 2026년 2월에 측정되었습니다. 폰트 서브셋 크기는 실제 사용하는 문자 수에 따라 달라집니다. Google Fonts API의 text 파라미터 동작은 변경될 수 있습니다. 본 콘텐츠의 비상업적 공유는 자유이나, 상업적 이용 시 문의 페이지를 통해 연락 바랍니다.
댓글
(4개)로그인하면 댓글을 작성할 수 있습니다.
미사용 weight 제거 팁이 실용적입니다. grep으로 font-serif가 항상 font-bold랑 같이 쓰이는지 확인하는 게 간단하면서 효과적이네요.
font-display: optional이 CLS도 막고 LCP 재측정도 방지한다는 게 깔끔하네요. swap만 쓰고 있었는데 optional로 전환 고려해봐야겠습니다.
Google Fonts CDN 비동기 로드하면 더 빠를 줄 알았는데 11점 하락이라니 충격입니다. 저희도 preconnect 넣어서 외부 CDN 쓰고 있었는데 바꿔야겠네요.
관련 글
© 2026 TreeRU. All rights reserved.
본 콘텐츠의 저작권은 TreeRU에 있으며, 출처를 밝히지 않은 무단 전재 및 재배포를 금합니다. 인용 시 출처(treeru.com)를 반드시 명시해 주세요.