- Published on
Chrome Layout Shift 폭증? font-display로 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서치 콘솔이나 CrUX 리포트에서 어느 날부터 Chrome의 CLS가 확 튀는 경우가 있습니다. 특히 배포 후 디자인을 건드리지 않았는데도 Layout shift 경고가 늘었다면, 이미지보다 먼저 웹폰트 로딩 정책을 의심하는 게 빠릅니다.
웹폰트는 네트워크 상태, 캐시, 크로스 오리진 설정, 브라우저의 폰트 렌더링 타이밍에 따라 텍스트의 폭과 줄바꿈이 바뀌기 쉽습니다. 이 변화가 화면에 이미 그려진 뒤에 발생하면 CLS로 잡힙니다. 이번 글은 font-display를 중심으로, Chrome에서 레이아웃 시프트를 안정적으로 줄이는 실전 조합을 다룹니다.
참고로 CLS 전반(이미지 크기 고정 포함)까지 한 번에 점검하고 싶다면 아래 글도 함께 보세요.
왜 Chrome에서 폰트가 CLS를 만들까
웹폰트가 CLS를 만드는 대표 시나리오는 아래 3가지입니다.
1) FOIT와 FOUT
- FOIT: 폰트가 로드될 때까지 텍스트를 숨김(투명) 처리했다가 한 번에 나타나는 방식
- FOUT: 일단 폴백 폰트로 보여주고, 웹폰트가 로드되면 교체
둘 다 교체 시점에 텍스트의 폭, 높이, 줄바꿈이 달라지면 레이아웃이 움직입니다. Chrome은 폰트 로딩과 페인트 타이밍에서 이 현상이 비교적 자주 관측됩니다.
2) 폴백 폰트 메트릭 불일치
웹폰트와 폴백 폰트의 ascent descent line-gap 및 글자 폭이 다르면, 폴백으로 그린 뒤 웹폰트로 바뀌면서 줄 높이와 줄바꿈이 바뀝니다. 특히 헤더, 버튼, 카드 타이틀처럼 한 줄로 고정된 UI에서 흔합니다.
3) 늦은 CSS 도착과 폰트 선언 타이밍
@font-face가 선언된 CSS가 늦게 도착하거나, 라우트 전환 후 특정 페이지에서만 폰트를 동적 로딩하면 “이미 그려진 텍스트”가 나중에 폰트 교체를 겪습니다. 이때 CLS가 튀기 쉽습니다.
핵심: font-display를 어떻게 써야 하나
@font-face의 font-display는 브라우저가 폰트를 기다릴지, 폴백으로 먼저 그릴지, 언제 교체할지를 결정합니다.
주로 쓰는 값은 아래 3개입니다.
swap: 즉시 폴백으로 렌더링하고, 웹폰트 로드 후 교체fallback: 짧게 기다리다가(매우 짧은 block) 폴백으로 렌더링, 늦게 로드되면 교체를 포기할 수 있음optional: 네트워크가 느리면 아예 웹폰트를 쓰지 않고 폴백 유지(CLS 최소화에 유리)
결론부터 말하면
- 브랜드 타이포가 아주 중요하지 않거나, CLS가 KPI라면
optional또는fallback이 유리합니다. - “무조건 웹폰트 적용”이 중요하면
swap을 쓰되, 폰트 메트릭을 맞추는 추가 조치가 필요합니다.
실전 설정 1: swap만 넣고 끝내면 왜 위험한가
많이들 아래처럼 설정합니다.
@font-face {
font-family: "MyBrand";
src: url("/fonts/MyBrand.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
이건 “보이기”는 좋아집니다(텍스트가 빨리 표시됨). 하지만 교체는 거의 확정이므로, 폴백과 웹폰트의 폭이 다르면 CLS가 발생합니다.
따라서 swap을 쓸 때는 아래 2가지를 같이 묶는 게 안전합니다.
- 폴백 폰트를 웹폰트와 최대한 비슷하게 선택
- 가능하면 메트릭 오버라이드로 줄 높이 변화를 억제
실전 설정 2: fallback 또는 optional로 CLS 자체를 줄이기
CLS를 확실히 줄이는 쪽으로 가면, “늦게 로드된 웹폰트는 교체하지 않도록” 만드는 전략이 효과적입니다.
@font-face {
font-family: "MyBrand";
src: url("/fonts/MyBrand.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: fallback;
}
또는 더 강하게:
@font-face {
font-family: "MyBrand";
src: url("/fonts/MyBrand.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: optional;
}
fallback: 적당히 웹폰트를 시도하되, 늦어지면 폴백 유지 가능optional: 네트워크가 느리면 웹폰트 적용 자체를 포기해 레이아웃 변화를 크게 줄임
브랜드 폰트가 핵심인 랜딩 페이지는 swap, 내부 앱 UI는 optional처럼 페이지 성격에 따라 나누는 것도 좋은 전략입니다.
실전 설정 3: 폰트 메트릭 오버라이드로 “줄 높이 점프” 막기
최근 브라우저들은 @font-face에서 폰트 메트릭을 오버라이드할 수 있습니다. 목적은 간단합니다. 폴백과 웹폰트의 세로 메트릭 차이를 줄여 교체 시 레이아웃 이동을 최소화합니다.
@font-face {
font-family: "MyBrand";
src: url("/fonts/MyBrand.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
/* 폴백과의 메트릭 차이를 줄이기 위한 오버라이드 */
ascent-override: 92%;
descent-override: 22%;
line-gap-override: 0%;
size-adjust: 102%;
}
주의할 점:
- 값은 폰트마다 다릅니다. 무작정 복붙하면 오히려 깨져 보일 수 있습니다.
- 접근 방식은 “폴백 폰트 기준으로 웹폰트를 미세 조정”하는 것입니다.
권장 워크플로우는 다음과 같습니다.
- 폴백 폰트를 먼저 정함(예:
system-ui,Arial,Noto Sans KR등) - 개발 환경에서 폴백과 웹폰트를 번갈아 적용하며 줄 높이와 줄바꿈 차이를 관찰
size-adjust와 오버라이드 값으로 차이가 가장 작은 지점을 찾음
실전 설정 4: 폰트 프리로드로 교체 타이밍을 앞당기기
swap을 유지해야 한다면 교체가 “사용자 눈에 보이기 전에” 일어나도록 폰트를 빨리 가져오는 게 중요합니다. 가장 단순하고 효과적인 방법이 프리로드입니다.
<link
rel="preload"
href="/fonts/MyBrand-Regular.woff2"
as="font"
type="font/woff2"
crossorigin
/>
체크리스트:
as는 반드시fonttype은 실제 포맷과 일치- 다른 도메인에서 폰트를 받으면
crossorigin필요 - 너무 많은 폰트를 프리로드하면 오히려 초기 로딩을 망칩니다. “첫 화면에 필요한 1~2개”만 프리로드하세요.
실전 설정 5: Next.js에서 폰트 로딩을 안정화하는 방법
Next.js라면 가능하면 next/font를 우선 고려하세요. 자동 최적화(서브셋, 프리로드, CSS 주입 타이밍)가 붙어 폰트로 인한 CLS가 줄어드는 경우가 많습니다.
// app/layout.tsx 또는 pages/_app.tsx
import localFont from "next/font/local";
const myBrand = localFont({
src: [
{
path: "../public/fonts/MyBrand-Regular.woff2",
weight: "400",
style: "normal",
},
],
display: "swap",
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko" className={myBrand.className}>
<body>{children}</body>
</html>
);
}
여기서도 display는 swap만 고집하기보다, UI 성격에 따라 optional을 검토해 볼 가치가 있습니다.
CLS가 “폰트 때문”인지 빠르게 확인하는 법
Chrome DevTools에서 다음 순서로 확인하면 원인 규명이 빨라집니다.
- Performance 패널에서 기록
- Experience 섹션의 Layout Shifts 확인
- shift가 발생한 시점에 어떤 노드가 움직였는지 확인
- 해당 시점에 폰트 로딩 이벤트가 있었는지(Network에서
woff2타이밍) 대조
추가로 INP나 Long Task가 함께 나쁘게 보인다면, 사용자 입력 타이밍에 폰트 교체가 겹쳐 체감이 더 나빠질 수 있습니다. 폰트 최적화와 별개로 메인 스레드 작업도 쪼개는 것이 도움이 됩니다.
권장 조합 정리
서비스에서 많이 쓰는 조합을 상황별로 정리하면 아래와 같습니다.
A안: CLS 최우선(브랜드 폰트는 옵션)
font-display: optional- 폴백 폰트 신중히 선택
- 프리로드는 최소화(정말 필요한 경우만)
B안: 타이포 중요(그래도 CLS는 줄이고 싶음)
font-display: fallback- 첫 화면 폰트 1개만 프리로드
- 가능하면 메트릭 오버라이드 적용
C안: 무조건 웹폰트 적용(디자인 일관성 최우선)
font-display: swap- 프리로드 적용
- 메트릭 오버라이드 및 폴백 튜닝 필수
마무리
Chrome에서 CLS가 갑자기 폭증하는 케이스는, 실제로는 “레이아웃을 바꾼 코드”가 아니라 “폰트가 바뀌는 타이밍” 때문에 발생하는 경우가 많습니다. font-display는 그 타이밍을 통제하는 가장 간단한 레버이고, 프리로드와 메트릭 오버라이드까지 조합하면 CLS를 눈에 띄게 안정화할 수 있습니다.
한 번에 다 바꾸기보다, 다음 순서로 적용해 보세요.
font-display를fallback또는optional로 실험- 첫 화면 폰트만 프리로드
- 그래도 필요하면 메트릭 오버라이드로 미세 조정
이 3단계만으로도 “Chrome에서만 CLS가 튄다”는 이슈의 상당수를 정리할 수 있습니다.