웹폰트가 뒤늦게 로드되면서 텍스트가 폴백 글꼴에서 진짜 글꼴로 바뀌는 순간, 줄 높이와 글자 폭이 달라져 화면이 툭 밀리는 현상 — 이게 폰트 교체로 인한 레이아웃 시프트(CLS)다. 폰트 폴백 메트릭 오버라이드는 CSS @font-face에 size-adjust·ascent-override 같은 속성을 지정해, 폴백 글꼴의 크기를 웹폰트와 똑같이 맞춰 이 밀림을 없애는 기법이다.
나는 오랫동안 이걸 "swap이냐 optional이냐" 고민으로만 접근했다. 그런데 진짜 해법은 폴백 글꼴 자체를 웹폰트와 같은 치수로 위장시키는 것이었다.
왜 밀리는가 — 글꼴마다 치수가 다르다
font-display: swap을 쓰면 웹폰트가 도착하기 전 폴백 글꼴로 먼저 그린다. 좋다. 문제는 폴백(예: Arial)과 실제 웹폰트의 문자 폭·줄 높이가 다르다는 것. 그래서 웹폰트가 도착해 교체되는 순간, 같은 문단이 몇 줄 늘거나 줄어들며 아래 콘텐츠가 통째로 밀린다. 사용자가 막 읽으려던 버튼이 손가락 밑에서 도망가는 그 불쾌한 경험이다.
해결 — 폴백을 웹폰트 치수로 위장
핵심 속성은 넷이다. 폴백 전용 @font-face를 하나 만들어 여기에 오버라이드를 건다.
| 속성 | 역할 |
|---|---|
| size-adjust | 글자 크기 배율 보정(폭·높이 전반) |
| ascent-override | 위쪽 여백(어센트) 높이 |
| descent-override | 아래쪽 여백(디센트) 높이 |
| line-gap-override | 줄 간격 |
/* 진짜 웹폰트 */
@font-face {
font-family: 'Pretendard';
src: url('/fonts/pretendard.woff2') format('woff2');
font-display: swap;
}
/* 웹폰트와 치수를 맞춘 폴백 */
@font-face {
font-family: 'Pretendard-fallback';
src: local('Arial');
size-adjust: 105%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
body {
font-family: 'Pretendard', 'Pretendard-fallback', sans-serif;
}
이제 폴백으로 그릴 때도 실제 웹폰트와 거의 같은 공간을 차지한다. 교체가 일어나도 레이아웃이 그대로라 시프트가 0에 수렴한다.
배율 값은 어떻게 구하나
손으로 감 잡아 넣지 말자. 두 가지 실전 방법이 있다.
- 도구에 맡기기. 폰트 파일에서 실제 메트릭을 읽어 오버라이드 값을 계산해주는 유틸리티들이 있다. Next.js의
next/font는 이 과정을 빌드 타임에 자동으로 해준다. 로컬 폰트든 구글 폰트든 알아서 폴백 @font-face를 만들어 붙인다. - 직접 계산. 웹폰트의 unitsPerEm 대비 ascent/descent 값을 폴백 글꼴 기준으로 환산한다. size-adjust는 (웹폰트 평균 글자폭 ÷ 폴백 평균 글자폭)에서 출발해 미세 조정한다.
개인적으로는 도구를 쓰라고 강권한다. 이 값은 폰트를 바꾸면 다시 계산해야 하는데, 손으로 관리하면 반드시 언젠가 어긋난다. 몇 번 데인 뒤에야 자동화로 넘어갔다.
font-display 옵션과의 관계
메트릭 오버라이드는 font-display를 대체하는 게 아니라 짝을 이룬다. 정리하면 이렇다.
swap+ 메트릭 오버라이드: 텍스트를 즉시 보여주면서 시프트도 없앤다. 대부분의 경우 최적.optional: 폰트가 아주 빨리 안 오면 그냥 폴백으로 확정. 시프트는 원천 차단되지만 첫 방문에 웹폰트가 안 보일 수 있다.block: 텍스트를 잠깐 숨긴다. FOIT를 유발하므로 접근성상 권하지 않는다.
자주 묻는 질문
size-adjust 하나만 써도 되나요?
글자 폭 위주의 시프트는 size-adjust만으로도 상당히 잡힙니다. 다만 줄 높이 차이가 크면 ascent/descent-override까지 함께 조정해야 완전히 사라집니다.
next/font를 쓰면 신경 안 써도 되나요?
네, next/font/google이나 next/font/local은 폴백 메트릭을 자동 계산해 CLS를 거의 0으로 만듭니다. 원리를 알면 왜 그게 되는지 이해하고 커스터마이징도 할 수 있습니다.
가변 폰트(variable font)에도 적용되나요?
됩니다. 오버라이드는 폴백 @font-face에 거는 것이라 웹폰트가 가변이든 정적이든 무관합니다.
local() 폴백이 사용자 기기에 없으면요?
그때는 다음 순위의 시스템 글꼴로 넘어갑니다. 그래서 src: local('Arial')처럼 널리 깔린 글꼴을 기준으로 잡고, family 목록 끝에 sans-serif 같은 제네릭을 두는 게 안전합니다.

댓글 0