한글 웹폰트 넣고 글자가 튀는 건 서브셋과 metric override 문제입니다

한글 웹폰트 완전 정리 — @font-face·서브셋·CLS 잡는 font metric override까지
font-display: swap을 넣었는데 PageSpeed Insights에서 CLS 점수가 나빠졌다는 피드백을 받은 적 있는가? 아니면 KoPub 폰트를 서브셋 변환까지 다 해놓고 나서야 "서버 임베딩은 별도 승인"이라는 문구를 발견한 경우는? 한글 웹폰트는 단계별 절차를 따라가면 금방 될 것 같지만, 실제로는 절차 사이사이에 숨어 있는 함정에서 시간이 날아간다.
이 글은 @font-face·서브셋·fallback 스택 같은 기본 절차를 정리하되, swap이 왜 CLS를 유발하는지, 그걸 CSS 한 블록으로 어떻게 잡는지를 중심에 놓는다. 대부분의 가이드가 "swap 쓰세요"에서 멈추는 부분에서 한 발 더 들어가는 게 이 글의 핵심이다.
한글 폰트가 유독 무거운 이유
영문 폰트는 알파벳 기본 세트 기준으로 파일 크기가 수십 KB 수준이다. 한글은 초·중·종성 조합으로 만들 수 있는 글자가 11,172자에 달해서 파일 크기가 근본적으로 다르다. ttf 원본 기준으로 많게는 16 MB까지 나간다. 무심코 웹 서버에 올리면 폰트 파일 하나 때문에 페이지 로딩이 수 초씩 지연된다.
해결책은 두 가지를 동시에 쓰는 것이다. 형식 변환(ttf → woff2)과 서브셋(필요한 글자만 추출). Noto Sans CJK KR Regular 원본(15 MB)을 KS X 1001 기준 2,350자로 서브셋하고 woff2로 변환하면 379 KB까지 줄어든다(출처: 44BITS). 계산하면 약 97% 감소다. 이 감소폭이 나오는 이유는 두 효과가 곱해지기 때문이다 — woff2의 Brotli 압축이 파일 구조를 압축하고, 서브셋이 글리프 수 자체를 11,172자에서 2,350자로 줄인다.
woff2는 2020년 이후 출시된 모든 모던 브라우저가 지원한다. 자체 호스팅이라면 woff2 하나만 선언해도 충분하고, 구형 IE 대응이 필요하지 않다면 woff를 별도로 준비할 이유가 없다.
woff2 서브셋 생성 실전
서브셋은 Python 환경의 fonttools로 만든다. 11,172자를 전부 싣지 않고 실제로 쓰이는 2,350자(KS X 1001 기준)만 추려내는 것으로, 일반 텍스트 콘텐츠에서 등장하는 한글 자모는 이 범위 안에 거의 다 포함된다.
# fonttools 설치
pip install fonttools brotli
# KS X 1001 유니코드 범위로 서브셋 생성 후 woff2 변환
pyftsubset NotoSansKR-Regular.ttf \
--unicodes="U+AC00-D7A3,U+0020-007E,U+3130-318F" \
--flavor=woff2 \
--output-file=NotoSansKR-subset.woff2
서브셋 파일이 여러 개라면 unicode-range를 함께 선언해 브라우저가 필요한 파일만 내려받도록 한다. 한글 전용 파일과 라틴 전용 파일을 분리하면, 라틴 문자만 쓰이는 페이지에서는 한글 파일이 아예 요청되지 않아 불필요한 네트워크 낭비를 줄일 수 있다.
@font-face 선언에 굵기와 unicode-range까지
서브셋 파일이 준비됐다면 CSS에 @font-face를 선언한다. 여기서 이름을 정해두면 나중에 어떤 요소에든 font-family로 참조해 쓸 수 있다.
/* 기본 단일 파일 방식 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
/* unicode-range로 한글 파일만 지정 */
@font-face {
font-family: 'MyFont';
src: url('/fonts/myfont-kr.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
unicode-range: U+AC00-D7A3, U+0020-007E, U+3130-318F;
}
굵기별로 파일이 다를 경우 font-weight 값만 달리해서 @font-face 블록을 여러 개 선언하면 되고, 브라우저는 적용 시점에 맞는 굵기 파일만 골라 내려받는다. font-family에 지정한 이름은 이후 font-family: 'MyFont', sans-serif; 형태로 참조한다.
font-display 선택과 CLS 함정 ⚠️
font-display는 폰트 파일이 로딩되는 동안 텍스트를 어떻게 보여줄지 결정한다. 여기서 많은 가이드가 "한글 본문에는 swap이 적합하다"고 끝낸다. 맞는 말이지만, swap이 새로운 문제를 만들 수 있다는 점은 빠뜨린다.
| 값 | 차단 구간 | 교체 구간 | 동작 요약 |
|---|---|---|---|
block | 짧음(~3초) | 무한 | 폰트 로딩 중 텍스트 숨김 → 로드 후 표시 |
swap | 극소(~100ms) | 무한 | 즉시 fallback 표시 → 로드 완료 시 교체 |
fallback | 극소(~100ms) | 약 3초 | swap과 유사하나 3초 초과 시 fallback 고정 |
optional | 극소(~100ms) | 없음 | 캐시에 있으면 사용, 없으면 fallback 고정 |
swap은 텍스트가 아예 안 보이는 FOIT보다 분명히 낫다. 문제는 교체 시점에 CLS(Cumulative Layout Shift)가 발생한다는 것이다. fallback 폰트(예: Apple SD Gothic Neo)와 웹폰트(예: Pretendard)는 ascent·descent·line-height 값이 다르다. 폰트가 교체되는 순간 행간과 텍스트 높이가 달라지면서 그 아래에 있는 버튼·이미지·단락 전체가 픽 밀린다. PageSpeed Insights에서 CLS 0.1 초과가 떴다면 swap 교체 시점이 원인일 가능성이 높다.
optional로 바꾸면 CLS는 완전히 없어지지만, 첫 방문에서 폰트가 아예 안 적용된다는 트레이드오프가 생긴다. 한글 콘텐츠 중심 사이트에서 첫 방문 시 시스템 기본 폰트로 보이는 건 감수하기 어려운 경우가 많다.
swap을 유지하면서 CLS를 줄이는 방법 — CSS font metric override
핵심은 fallback 폰트의 메트릭을 웹폰트에 맞게 조정하는 것이다. CSS @font-face의 ascent-override, descent-override, line-gap-override, size-adjust 속성을 이용하면 시스템 폰트가 웹폰트와 비슷한 크기와 행간으로 그려지도록 조정할 수 있다. 교체 전후 레이아웃 차이가 줄어들어 CLS가 감소한다.
/* fallback 폰트에 웹폰트 메트릭을 맞추는 예시 */
/* 아래 퍼센트 값은 실제 폰트마다 다르며, 예시 값이다 */
@font-face {
font-family: 'PretendardFallback';
src: local('Apple SD Gothic Neo'), local('Malgun Gothic');
ascent-override: 90%; /* 글자 위쪽 여백 조정 */
descent-override: 25%; /* 글자 아래쪽 여백 조정 */
line-gap-override: 0%; /* 행간 여백 조정 */
size-adjust: 95%; /* 전체 크기 조정 */
}
body {
font-family: 'Pretendard', 'PretendardFallback', sans-serif;
}
이 블록의 역할은 Pretendard가 로드되기 전에 PretendardFallback이 비슷한 공간을 차지하도록 만드는 것이다. 폰트가 교체될 때 레이아웃이 재계산되는 폭이 줄어들어 CLS 점수가 개선된다. 정확한 ascent-override 값은 폰트마다 다르므로, 각 폰트의 실제 메트릭을 확인하거나 브라우저 DevTools에서 실측해 맞춰야 한다. MDN Web Docs의 @font-face 문서에서 각 속성의 허용 범위와 의미를 확인할 수 있다.
로고나 헤드라인처럼 브랜드 일관성이 중요한 자리라면 fallback을 쓴다. 3초 안에 로드되지 않으면 그 페이지 방문에서는 시스템 폰트로 고정되므로, 네트워크가 느린 환경에서도 레이아웃이 안정적으로 유지된다.
fallback 스택과 Google Fonts CDN 선택 기준
웹폰트가 로딩되지 않았을 때 어떤 폰트로 대신 보여줄지도 챙겨야 한다. fallback 폰트는 실제로 화면에 보이는 시간이 있고, 위에서 설명한 metric override도 fallback 폰트를 어떤 것으로 지정하느냐에 따라 효과가 달라진다.
body {
font-family: 'MyFont', 'Apple SD Gothic Neo', 'Noto Sans KR',
'Malgun Gothic', sans-serif;
}
- macOS/iOS: Apple SD Gothic Neo
- Windows: Malgun Gothic (맑은 고딕)
- Android: Noto Sans KR (시스템 내장)
- 최후 fallback:
sans-serif
Noto Sans KR이나 나눔고딕처럼 Google Fonts에 올라간 폰트라면 CDN을 그냥 쓸 수도 있다. 태그 한 줄이면 설정이 끝나고, Google이 브라우저 환경에 맞게 자동으로 최적화된 파일을 내려준다. 다만 속도·제어·개인정보 측면에서 자체 호스팅과 차이가 있다.
| 항목 | Google Fonts CDN | 자체 호스팅 |
|---|---|---|
| 설정 편의성 | 태그 한 줄 | 파일 업로드 + CSS 작성 |
| 로딩 성능 | DNS + TCP 연결 추가 발생 | 추가 외부 연결 없음 |
| 개인정보 | Google에 접속 IP 전달 | 없음 |
| 제어권 | 버전 자동 변경 가능 | 완전 제어 |
| GDPR | 별도 고지 필요할 수 있음 | 문제 없음 |
브라우저 캐시 공유 효과는 최신 브라우저 프라이버시 정책(파티션 캐시)으로 사실상 사라진 상태라, CDN의 성능 이점도 이전만큼 크지 않다. CLS를 직접 제어하고 싶다면 자체 호스팅이 유리하다. CDN 방식에서는 Google이 내려주는 @font-face 선언을 그대로 쓰게 되어, 위에서 설명한 metric override 블록을 삽입하기 번거롭다. 자체 호스팅이라면 CSS를 완전히 제어할 수 있어 fallback 메트릭 조정도 자유롭다.
KoPub 임베딩은 사전 승인이 필요합니다
폰트를 서버에 올려 배포하는 건 단순 사용과 다르다. 로컬에서 문서 작업에 쓰는 것과 달리, 서버에 올려 불특정 다수에게 파일을 전송하는 행위는 배포에 해당한다. woff2 변환까지 다 마쳐두고 라이선스를 확인하면 뒤집어야 하는 상황이 생긴다.
| 폰트명 | 제작사 | 라이선스 | 웹 임베딩 | 비고 |
|---|---|---|---|---|
| Pretendard | orioncactus | OFL 1.1 | 허용 | 수정·재배포 허용, 폰트 단독 판매만 금지 |
| 나눔고딕 / 나눔바른고딕 | 네이버 | OFL 1.1 | 허용 | 상업용 포함 자유 사용 |
| Noto Sans KR | Google / Adobe | OFL 1.1 | 허용 | Google Fonts CDN 또는 자체 호스팅 모두 가능 |
| 본고딕 (Source Han Sans KR) | Adobe | OFL 1.1 | 허용 | Noto Sans와 동일 글리프 세트 |
| KoPub 바탕 / 돋움 | 한국출판인회의 | 자체 라이선스 | 별도 승인 필요 | 서버 탑재 후 웹서비스 임베딩은 사전 승인 필수 |
| KoPubWorld | 한국출판인회의 | 자체 라이선스 | 별도 승인 필요 | 홈페이지 등록 후 사용, 임베딩 승인 별도 |
OFL(SIL Open Font License) 폰트는 웹폰트로 삽입해 배포하는 행위를 명시적으로 허용한다. 수정·재배포도 가능하고, 폰트 파일을 단독으로 판매하는 경우에만 제한이 붙는다.
KoPub 계열이 함정이다. 인쇄나 온라인 콘텐츠 결과물은 자유롭지만, 폰트 파일 자체를 서버에 올려 서비스하는 임베딩은 한국출판인회의의 별도 승인이 필요하다(한국출판인회의 공식 페이지 참조). 조판이 예쁘다는 이유로 KoPub 바탕을 선택했다가 서비스 직전에 이 조항을 발견하면 폰트를 통째로 갈아야 한다. KoPub를 쓰고 싶다면 서브셋 생성 전에 한국출판인회의에 먼저 문의하는 것이 순서다.
라이선스 조건은 언제든 바뀔 수 있으니, 적용 전에 공식 페이지에서 최신 내용을 직접 확인하자.
폰트비에서 라이선스 확인과 @font-face 코드 복사
어떤 폰트를 쓸지 정했다면, 라이선스 확인과 코드 작성을 한 번에 해결할 수 있다. 폰트비의 각 폰트 상세 페이지(/fonts/)에는 웹 임베딩을 포함한 사용 범위 표시(인쇄·웹사이트·영상·포장지·임베딩·BI/CI·OFL 가능 여부)와 함께 @font-face 코드 복사 버튼이 있다. 복사 버튼을 누르면 CDN URL이 포함된 완성 코드가 클립보드에 담기고, CSS에 붙여넣기만 하면 된다. 같은 페이지에서 라이선스 안내 원문도 확인할 수 있어, 웹 임베딩이 가능한지 따로 검색할 필요 없이 한 화면에서 확인이 끝난다.
폰트비는 파일 배포처가 아니라 디렉터리다. 폰트 파일 자체는 각 제작사 공식 배포처에서 받고, 폰트비는 라이선스 확인과 코드 복사 편의를 제공한다. KoPub처럼 임베딩이 제한된 폰트라면 상세 페이지의 사용 범위 표시에서 해당 항목이 비활성 상태로 표시되므로, 전환 작업에 착수하기 전에 여기서 먼저 확인하는 것이 빠르다.
서체를 처음 고르는 단계라면 AI 폰트 찾기(/find)에서 용도나 분위기를 입력해 후보를 좁힐 수 있고, 카테고리별 추천(/recommend)이나 최근 등록된 폰트(/new)도 둘러볼 수 있다. 수백 종을 일일이 미리보기하지 않아도 원하는 방향의 폰트를 빠르게 찾을 수 있다.
참고자료
- 44BITS, "웹폰트 경량화: 폰트툴즈(fontTools)의 pyftsubset을 사용한 폰트 서브셋 만들기" — https://www.44bits.io/ko/post/optimization_webfont_with_pyftsubnet
- MDN Web Docs, "@font-face: font-display" — https://developer.mozilla.org/ko/docs/Web/CSS/Reference/At-rules/@font-face/font-display
- 한국출판인회의, "KoPubWorld 서체 라이선스" — https://www.kopus.org/biz-electronic-font2/
이 글이 도움이 됐나요?
여러분의 반응이 다음 글의 방향이 돼요


