Published on

Chrome Layout Shift 폭증? font-display로 해결

Authors

서치 콘솔이나 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-facefont-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가지를 같이 묶는 게 안전합니다.

  1. 폴백 폰트를 웹폰트와 최대한 비슷하게 선택
  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%;
}

주의할 점:

  • 값은 폰트마다 다릅니다. 무작정 복붙하면 오히려 깨져 보일 수 있습니다.
  • 접근 방식은 “폴백 폰트 기준으로 웹폰트를 미세 조정”하는 것입니다.

권장 워크플로우는 다음과 같습니다.

  1. 폴백 폰트를 먼저 정함(예: system-ui, Arial, Noto Sans KR 등)
  2. 개발 환경에서 폴백과 웹폰트를 번갈아 적용하며 줄 높이와 줄바꿈 차이를 관찰
  3. size-adjust와 오버라이드 값으로 차이가 가장 작은 지점을 찾음

실전 설정 4: 폰트 프리로드로 교체 타이밍을 앞당기기

swap을 유지해야 한다면 교체가 “사용자 눈에 보이기 전에” 일어나도록 폰트를 빨리 가져오는 게 중요합니다. 가장 단순하고 효과적인 방법이 프리로드입니다.

<link
  rel="preload"
  href="/fonts/MyBrand-Regular.woff2"
  as="font"
  type="font/woff2"
  crossorigin
/>

체크리스트:

  • as는 반드시 font
  • type은 실제 포맷과 일치
  • 다른 도메인에서 폰트를 받으면 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>
  );
}

여기서도 displayswap만 고집하기보다, UI 성격에 따라 optional을 검토해 볼 가치가 있습니다.

CLS가 “폰트 때문”인지 빠르게 확인하는 법

Chrome DevTools에서 다음 순서로 확인하면 원인 규명이 빨라집니다.

  1. Performance 패널에서 기록
  2. Experience 섹션의 Layout Shifts 확인
  3. shift가 발생한 시점에 어떤 노드가 움직였는지 확인
  4. 해당 시점에 폰트 로딩 이벤트가 있었는지(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를 눈에 띄게 안정화할 수 있습니다.

한 번에 다 바꾸기보다, 다음 순서로 적용해 보세요.

  1. font-displayfallback 또는 optional로 실험
  2. 첫 화면 폰트만 프리로드
  3. 그래도 필요하면 메트릭 오버라이드로 미세 조정

이 3단계만으로도 “Chrome에서만 CLS가 튄다”는 이슈의 상당수를 정리할 수 있습니다.