블로그 폰트 최적화 — 11,172자의 무게 '줄이기'
Astro 블로그에서 한글 웹 폰트의 깜빡임을 줄이기 위해 FOIT/FOUT의 차이부터 WOFF2 변환, Preload, Static Subset까지 적용한 구현 기록입니다.
블로그를 운영하다 보면, 한글 웹 폰트가 처음 로드될 때 글자가 깜빡이거나 폰트가 바뀌는 현상을 자주 마주칩니다. 페이지가 빠르게 보이는 것도 중요하지만, 사용자가 페이지를 읽기 시작한 직후 글자가 다른 모양으로 한 번 더 바뀌는 경험은 꽤 신경쓰입니다.
한글 폰트는 영문 폰트와 달리 11,172자나 되는데, 이걸 어떻게 줄여서 빠르게 보여줄 수 있을까?
첫 화면에서 글자가 깜빡이거나 꿀렁이는 현상을 어떻게 줄일 수 있을까?
이 글에서는 FOIT과 FOUT의 차이를 짚고, Astro 블로그에 WOFF2 변환·Preload·Static Subset을 적용해 한글 웹 폰트의 깜빡임을 줄인 구현 기록을 정리합니다.
FOIT — 폰트가 다운로드될 때까지 글자를 숨긴다
FOIT 는 Flash of Invisible Text의 약자로, 웹 폰트 다운로드가 완료될 때까지 텍스트를 화면에 아예 보여주지 않는 방식입니다. 다운로드가 끝나면 텍스트가 한 번에 나타나므로 디자인이 깔끔하게 유지됩니다.
단점은 네트워크가 느리면 사용자가 최대 수 초 동안 글자가 전혀 없는 빈 화면을 보게 된다는 점입니다.
사용자 경험(UX) 관점에서는 치명적입니다. 그래서 일반적으로는 사용자가 콘텐츠를 즉시 읽을 수 있는 FOUT 방식을 의도적으로 유도하면서, 그 부작용인 깜빡임을 최소화하는 방향으로 최적화를 진행합니다.
font-display: swap으로 FOUT 유도
CSS의 @font-face 설정에 다음을 추가하면 브라우저에 FOUT 방식을 강제할 수 있습니다.
@font-face {
font-display: swap;
}
이렇게 하면 브라우저는 폰트가 로드되기 전까지 시스템 기본 폰트( Fallback font )로 글자를 먼저 보여주고, 웹 폰트가 도착하는 순간 교체합니다. FOIT 문제는 해결되지만, 이번에는 폰트가 교체되는 순간 글자가 꿀렁거리는 FOUT 현상이 발생합니다.
FOUT — 폰트가 바뀌면서 글자가 꿀렁인다
FOUT 는 Flash of Unstyled Text의 약자로, 웹 폰트가 로드되기 전까지 시스템 기본 폰트로 글자를 먼저 보여주는 방식입니다. 텍스트를 사용자가 즉시 읽을 수 있어 체감 속도가 빨라지지만, 폰트가 교체되는 순간 글자 폭과 높이가 달라지면서 레이아웃이 미세하게 흔들리는 문제가 있습니다.
이 현상을 완전히 없애기는 어렵지만, 웹 폰트가 빠르게 도착할수록 흔들림의 시간과 폭이 줄어듭니다.
즉, FOUT를 해결하는 핵심은 웹 폰트를 최대한 빨리 다운로드하고 렌더링하는 것입니다. 아래 세 가지 방법을 순서대로 적용했습니다.
1. WOFF2 포맷 사용
OTF와 TTF는 용량이 크다
기본적으로 폰트 파일의 포맷은 OTF(OpenType Font) 또는 TTF(TrueType Font) 입니다. 둘 다 벡터 기반 폰트로 확대해도 깨지지 않으며, 차이는 곡선을 표현하는 방식과 고급 타이포그래피 기능 지원 여부에 있습니다.
OTF와 TTF의 차이점은 뭘까?
- OTF는 CFF(PostScript) 곡선을 쓰며, 합자(ligature)·다양한 자형(stylistic set) 등 디자인 프로그램에서 활용되는 고급 기능을 더 풍부하게 지원합니다.
- TTF는 TrueType 곡선을 쓰며, 일반적인 문서 작성과 화면 렌더링에 폭넓게 사용됩니다.
문제는 두 포맷 모두 고해상도 원본 폰트를 그대로 담고 있어 용량이 크다는 점입니다. 웹에서 매번 이 파일을 다운로드하는 것은 비효율적입니다.
WOFF2 — 웹 전용으로 압축된 포맷
WOFF2 는 Web Open Font Format 2.0의 약자로, 웹 전송을 목적으로 설계된 폰트 포맷입니다. Brotli 기반 압축을 적용해 동일한 폰트라도 OTF/TTF 대비 약 30% 작은 용량을 갖습니다. 모든 현대 브라우저가 지원하기 때문에 웹에서는 사실상 표준 포맷으로 자리잡았습니다.
이 블로그에서도 모든 폰트 파일을 WOFF2로 변환해 사용했습니다. 같은 폰트라도 사용자에게 도달하는 시간이 줄어들고, 그만큼 FOUT의 흔들림 시간도 짧아집니다.
2. Font Preload — 폰트 다운로드 우선순위 끌어올리기
브라우저는 기본적으로 HTML을 파싱하고 CSS를 분석한 뒤에야 폰트 파일을 다운로드합니다. 즉 폰트는 보통 렌더링이 시작된 뒤에야 요청됩니다. <link rel="preload"> 태그를 HTML <head>에 추가하면, 브라우저가 HTML을 읽자마자 폰트 파일을 우선순위 높게 다운로드하도록 지시할 수 있습니다.
<link rel="preload" href="/fonts/KakaoBigSans-Regular.woff2" as="font" type="font/woff2" crossorigin />
<link rel="preload" href="/fonts/KakaoBigSans-Bold.woff2" as="font" type="font/woff2" crossorigin />
as="font"로 리소스 유형을 명시하고, crossorigin 속성을 함께 적어줘야 폰트가 정상적으로 캐싱됩니다. 빠뜨리면 브라우저가 같은 폰트를 두 번 받는 일이 생기니 주의가 필요합니다.
Preload 적용 전후 비교
이 블로그의 BaseLayout.astro에 위 코드를 추가한 뒤 측정해 봤습니다.
Before

After

폰트가 다른 리소스보다 먼저 요청되어 도착 시점이 앞당겨졌습니다. 차이가 극적이지는 않지만, 첫 화면 렌더링에서 폰트 교체가 일어나는 시점이 분명히 빨라졌습니다.
3. Static Subset — 한글 11,172자의 무게 줄이기
Preload와 WOFF2까지 적용했지만, 한글 폰트는 여전히 무겁습니다. 그 이유는 한글이 영문보다 압도적으로 글자 수가 많기 때문입니다.
영문 72자 vs 한글 11,172자
영문 폰트는 알파벳 대소문자와 기호 등 72자 안팎이면 충분해서 용량이 매우 작습니다.
반면, 한글은 자음과 모음의 조합으로 만들 수 있는 글자 수가 약 11,172자나 됩니다. 이 때문에 전체 한글 폰트 파일의 원본 용량은 수 MB에 달하며, 웹페이지 로딩 속도를 느리게 만드는 주범이 됩니다.
해결책은 Static Subset(정적 서브셋)입니다.
전체 폰트 파일에서 미리 정해진 글자 집합만 골라 크기를 줄여둔 폰트 파일입니다. 쉽게 말하면 “뷃, 뒑, 홁, 폛” 같은 일상적으로 쓰이지 않는 글자는 제외하고, 실제로 사용되는 글자만 포함한 폰트를 만드는 개념입니다.
Static Subset vs Dynamic Subset
서브셋에는 정적 서브셋에 대비되는 Dynamic Subset(동적 서브셋)도 있습니다.
동적 서브셋은 전체 폰트를 수십 개의 조각으로 잘게 쪼개 서버에 저장한 뒤, 페이지 내 실제 글자가 포함된 조각만 선별적으로 로드합니다.
동적 서브셋이 빛을 발하는 곳은 불특정 사용자가 많고 실시간으로 콘텐츠가 쌓이는 서비스입니다. 댓글·게시판처럼 사용자 입력에 따라 어떤 글자가 등장할지 예측이 어려운 경우, 동적 서브셋이 더 유리합니다.
현재 이 블로그는 Astro 프레임워크로 만들어진 정적 블로그입니다. 어떤 글자가 어디에 쓰일지 빌드 시점에 모두 알 수 있기 때문에, 정적 서브셋이 더 적합하다고 판단했습니다.
적용 과정
정적 서브셋을 만들기 위해 다음 두 단계를 거쳤습니다.
1) 사용 문자 추출 스크립트
scripts/generate-font-subset-text.mjs에서 src/content, pages, layouts, components, lib 디렉토리를 스캔해 실제로 사용된 모든 문자를 모은 뒤, public/fonts/subset-chars.txt에 저장합니다.
2) 폰트 서브셋 생성 스크립트
scripts/font-subset.mjs에서 4가지 폰트 파일(light, regular, bold, extrabold)에 대해 pyftsubset을 실행해 public/fonts/*.subset.woff2로 출력합니다.
const r = spawnSync("pyftsubset", [inputAbs, `--text-file=${textFile}`, "--flavor=woff2", `--output-file=${outputAbs}`], {
cwd: REPO_ROOT,
stdio: "inherit",
encoding: "utf8",
});
pyftsubset 은 bash 명령어로 직접 실행하지 않고 Node.js 스크립트로 감싼 이유는, 프로젝트 내에서 실제로 사용된 문자만 자동으로
추출해 매번 일관된 서브셋을 만들기 위해서입니다. 수동으로 문자 집합을 관리하면 새 글을 쓸 때마다 누락이 생길 수 있습니다.
3) package.json 스크립트 등록
빌드 파이프라인에 쉽게 통합할 수 있도록 npm script로 등록했습니다.
{
"scripts": {
"font:subset": "node scripts/font-subset.mjs"
}
}
이제 pnpm font:subset 한 줄로 서브셋 생성이 끝납니다.
빌드 후 폰트 크기

원본 폰트 대비 용량이 317kb -> 48kb로 크게 줄었습니다. 한글 11,172자에서 실제 블로그에 등장하는 글자 수백 자로 압축했기 때문입니다.
마지막으로 서브셋이 적용된 폰트 CSS를 <head> 안에 인라인으로 삽입해, 별도의 CSS 파일 요청 없이 폰트가 즉시 적용되도록 했습니다.
import fontsCss from "@/styles/fonts.css?raw";
<style is:inline set:html={fontsCss} />
결과
세 가지 기법을 모두 적용한 뒤의 결과입니다.
처음에 봤던 폰트 꿀렁임이 눈에 띄게 줄었습니다. WOFF2 변환과 Preload로 폰트 도착 시점을 앞당기고, 정적 서브셋으로 파일 크기 자체를 줄인 덕분에, 시스템 폰트에서 웹 폰트로 교체되는 시간이 짧아진 결과입니다.
마치며
한글 웹 폰트는 영문과 달리 11,172자라는 거대한 무게를 짊어지고 있습니다. 이걸 그대로 다운로드하게 두면, FOIT(빈 화면)이든 FOUT(꿀렁임)이든 사용자 경험이 나빠질 수 있다고 생각합니다. 직접 적용하면서 세 가지가 확실히 와닿았습니다.
- FOUT는 문제가 아니라 의도된 동작이고, 진짜 해결해야 할 건 폰트가 교체되는 순간의 흔들림 시간입니다. 흔들림을 없애려 하지 말고, 짧게 만들어야 합니다.
- WOFF2·Preload·Static Subset은 각각 다른 레이어를 건드립니다. WOFF2는 파일 자체의 압축, Preload는 다운로드 우선순위, Static Subset은 파일에 담긴 문자 수. 셋이 곱해질 때 효과가 가장 큽니다.
- 정적 서브셋의 장점은 빌드 타임에 결정되는 글자만 포함한다는 점입니다. 정적 사이트에서는 동적 서브셋보다 단순하고 빠른 선택지가 됩니다.
다만 글 제목을 “없애기”가 아닌 “줄이기”로 정한 데는 이유가 있습니다.
세 가지 기법을 모두 적용했지만, 첫 로드 시 FOUT를 “완전히” 없애지는 못했습니다. 캐시가 없는 첫 방문에서는 여전히 폰트 교체 순간이 느껴집니다.
font-display: optional처럼 교체 자체를 포기하는 방식도 있지만, 그러면 캐시가 없을 때 웹 폰트가 아예 적용되지 않아 트레이드오프가 큽니다. CSS size-adjust나 ascent-override로 Fallback font를 웹 폰트 크기에 맞게 미리 조정하는 방법도 있다는 걸 알았지만, 아직 제대로 파고들지 못했습니다.
FOUT를 완전히 없애는 건 하나의 과제로 생각하고 더 파고들어보겠습니다.