웹 폰트 개념 및 Next.js + tailwind 에서 한글 웹폰트 최적화하기
서론
웹 폰트는 사용자 경험에 직접 영향을 미칩니다. 큰 파일 크기는 LCP(Largest Contentful Paint) 같은 Core Web Vitals 지표를 악화시킵니다. 특히 한글 폰트는 영문 폰트보다 문자 수가 많아 파일 크기가 큽니다. 따라서 적절한 폰트 파일을 import하여 사용자 경험을 향상시켜야합니다.
이 글에서는 브라우저 폰트에 대한 개념을 알아보고 Next.js + tailwind 프로젝트에 Pretendard 폰트를 적용하고 최적화한 경험을 다룹니다.
본론
1. 폰트 포맷의 진화: woff vs woff2
웹 폰트 포맷의 역사
웹 폰트는 TTF/OTF, EOT(IE 전용), WOFF, WOFF2로 진화했습니다
WOFF2는 가장 높은 압축률로 더 작은 파일 크기를 가져 빠른 로딩 속도를 가집니다. IE 브라우저 지원이 필요하지 않다면 WOFF2를 사용하는 것을 권장합니다.
| 포맷 압축률 | 지원 브라우저 | 권장 여부 | Pretendard Bold 기준 파일 크기 |
|
| TTF/OTF | 압축 없음 | 대부분 지원 | ❌ (웹에서 비권장) | ~500KB |
| WOFF | Gzip 압축 | IE9+ 포함 전체 | △ (레거시 브라우저용) | ~342KB |
| WOFF2 | Brotli 압축 | 모던 브라우저 (97%+) | ✅ (권장) | ~264KB |
2. Variable Font vs Subset Font: 무엇을 선택할까?
Variable Font란?
Variable Font는 하나의 파일에 모든 굵기(weight)와 스타일을 포함하는 폰트입니다.
/* Variable Font 예시 */
@font-face {
font-family: 'Pretendard Variable';
src: url('/fonts/PretendardVariable.woff2') format('woff2-variations');
font-weight: 1 999; /* 모든 굵기 지원 */
}
/* 사용 시 */
.text {
font-weight: 450; /* 중간값도 사용 가능! */
}
Variable Font의 장단점:
| 👍 장점 | 👎 단점 |
|
|
Subset Font란?
Subset Font는 필요한 문자와 굵기만 포함한 최적화된 폰트입니다.
// Subset 전략 예시
한글 완성형: 11,172자 → 파일 크기 매우 큼 (1-2MB)
한글 상용 2,350자 + 영문/숫자 → 대폭 감소 (200-300KB)
Subset Font를 사용하면 파일 크기가 감소하여 더 빠른 초기 로딩을 할 수 있습니다.
따라서 특정 굵기만을 디자인에서 사용한다면 subset 한글 폰트 사용을 권장합니다.
3. 폰트가 웹 성능에 미치는 영향
개발자도구 Core Web Vitals 중 폰트와 관련된 지표:
1. LCP (Largest Contentful Paint)
- 목표: 2.5 sec 이하
- 큰 폰트 파일은 다운로드를 지연시켜 LCP를 증가시킵니다.
- FOIT(Flash of Invisible Text)는 콘텐츠 렌더링을 지연시킵니다.
2. CLS (Cumulative Layout Shift)
- 목표: 0.1 sec 이하
- 폰트 로딩 전후 크기 차이는 레이아웃 이동을 유발합니다.
- 대체 폰트와 메인 폰트의 크기 불일치가 원인입니다.
폰트 로딩 전략
브라우저의 폰트 로딩 방식의 종류는 다음과 같습니다.
font-display: auto; /* 브라우저 기본값 (~3s FOIT) */
font-display: block; /* FOIT, 최대 3초 대기 */
font-display: swap; /* FOUT, 즉시 대체 폰트 표시 */
font-display: fallback; /* 짧은 대기 후 대체 폰트 */
font-display: optional; /* 네트워크 상태에 따라 결정 */
이 종류 중 font-display: swap를 사용하는 것을 권장합니다.
swap은 FOUT 라는 웹 폰트 로딩 방식을 사용합니다.
폰트가 렌더링되기 전까지 대체 폰트를 사용자게에 보여주어, 사용자에게 바로 텍스트를 보여줄 수 있으며 빈화면이 사용자에게 보여지는 과정을 줄입니다. 웹 폰트 로딩이 완료되면 폰트를 전환하게 되는데, 이 과정에서 글꼴의 자간, 높이 등 서식이 달라 웹 폰트 적용 전과 후에 레이아웃이 변경될 수 있습니다. 하지만 빠른 렌더링으로 FCP와 LCP 성능을 향상시킵니다..
- FCP(First Contentful Paint): 브라우저가 페이지의 첫 번째 요소(일반적으로 텍스트)를 렌더링한 시점입니다. 사용자가 페이지를 요청한 후 FCP까지 걸리는 시간이 짧을수록 페이지는 빠르게 로드된 것으로 간주됩니다.
- LCP(Largest Contentful Paint): 페이지에서 가장 큰 요소(일반적으로 이미지 또는 비디오)가 렌더링된 시점입니다. LCP 시간은 페이지의 로딩 시간과 사용자 경험에 큰 영향을 미칩니다.
실제 성능 비교
700(bold), 500(medium), 400(regular) 굵기만 사용할때 Variable 대신 subset을 선택한다면 다음과 같이 더 가벼운 파일을 빠르게 다운로드할 수 있습니다.
// Before: Variable Font
파일 크기: 2.1MB
다운로드 시간 (throttling 없음): 129ms
// After: Subset Font (3개 굵기)
파일 크기: 806KB (3개 합계)
다운로드 시간 (throttling 없음): 18ms, 18ms, 22ms
4. Next.js의 폰트 최적화: next/font
Next.js의 next/font 는 폰트를 내부에서 self-hosting하여 자동 preload 하는 기능을 통해 폰트 로딩을 최적화합니다:
layout.tsx에서 pretendard.variable로 --font-pretendard CSS 변수를 정의합니다.
// app/layout.tsx
import localFont from 'next/font/local';
const pretendard = localFont({
src: [
{
path: '../fonts/Pretendard-Regular.subset.woff2',
weight: '400',
style: 'normal',
},
{
path: '../fonts/Pretendard-Medium.subset.woff2',
weight: '500',
style: 'normal',
},
{
path: '../fonts/Pretendard-Bold.subset.woff2',
weight: '700',
style: 'normal',
},
],
display: 'swap',
variable: '--font-pretendard',
});
export default function RootLayout({ children }) {
return (
<html lang="ko">
<body className={`${pretendard.variable}`}>
{children}
</body>
</html>
);
}
globals.css에서 body에서 정의한 CSS 변수를 font-family로 지정합니다.
body {
font-family: var(--font-pretendard), sans-serif;
@apply bg-background text-foreground;
}
5. Next.js + tailwind 프로젝트에 pretendard 폰트 적용하기
Next.js + tailwind 프로젝트에 폰트를 적용하는 과정을 정리하면 다음과 같습니다.
Step 1: 폰트 파일 준비
# 프로젝트 구조
src/
├── fonts/
│ ├── Pretendard-Regular.subset.woff2 # 400
│ ├── Pretendard-Medium.subset.woff2 # 500
│ └── Pretendard-Bold.subset.woff2 # 700
└── app/
└── layout.tsx
Step 2: layout.tsx 설정
// src/app/layout.tsx
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import './globals.css';
const pretendard = localFont({
src: [
{
path: '../fonts/Pretendard-Regular.subset.woff2',
weight: '400',
style: 'normal',
},
{
path: '../fonts/Pretendard-Medium.subset.woff2',
weight: '500',
style: 'normal',
},
{
path: '../fonts/Pretendard-Bold.subset.woff2',
weight: '700',
style: 'normal',
},
],
display: 'swap',
variable: '--font-pretendard',
});
export const metadata: Metadata = {
title: 'My App',
description: 'Optimized with subset fonts',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body className={`${pretendard.variable} font-pretendard antialiased`}>
{children}
</body>
</html>
);
}
Step 3: Tailwind CSS 설정
/* globals.css ( tailwind v4 ) */
@layer base {
body {
font-family: var(--font-pretendard), sans-serif; // sans-serif 는 대체 폰트
}
}
결론
어떤 프로젝트를 진행하든 가장 기본 세팅요소 중 하나는 폰트인 것 같습니다. 이 글을 작성하며 파편적으로 분산되어있던 웹폰트에 대해 정리할 수 있었으며 실제 사이드프로젝트에서 디자인 시스템 구축을 위한 폰트 세팅을 하면서 활용해볼 수 있었습니다. 디자인 시스템을 구축한 과정도 공유하겠습니다.
🙇♂️ 참고문헌