폰트 CLS(Cumulative Layout Shift) 최적화

기운찬곰·2023년 5월 16일
2
post-thumbnail

Overview

이번 시간에는 폰트에 대해 알아보겠습니다. 이미지와 더불어 폰트는 프론트엔드 입장에서는 매우 중요하면서도 까다로운 녀석이라고 할 수 있겠습니다. 그만큼 성능에 직접적인 영향을 미치기 때문인데요. 그래서 무엇보다 성능을 고려해서 최적화를 해주는 게 좋습니다.

그중에서도 저는 폰트에서 CLS(Cumulative Layout Shift)를 최적화하는 방법에 대해 다뤄볼까 합니다.


CLS(Cumulative Layout Shift)란?

일단, CLS에 대해 간단하게 알아보도록 하겠습니다.

누적 레이아웃 이동(CLS)은 사용자가 예상치 못한 레이아웃 이동을 경험하는 빈도를 수량화하므로 시각적 안정성을 측정할 때 중요한 사용자 중심 메트릭입니다. CLS가 낮으면 우수한 사용자 경험을 보장하는 데 도움이 됩니다. - web.dev 참고

Core Web Vitals 중 하나인 CLS는 그만큼 성능 지표에서 중요한 평가 항목이라고 볼 수 있습니다.

흔히 갑자기 안보이던 이미지가 나타나서 원래 눌러야 할 곳을 잘못 눌러본 경험 한번씩은 있을 것입니다. 이건 매우 좋지 못한 사용자 경험일 것입니다. 만약 CLS 성능 지표를 잘 지킨 사이트라면 그럴 일은 없었을 것입니다.


폰트와 Layout Shift

폰트마다 크기가 다르다

그렇다면 폰트는 Layout Shift에 영향을 줄 수 있을까요? 네. 보통은 이미지를 많이 떠올릴텐데 폰트도 영향을 줄 수 있습니다.

참고 : https://codepen.io/web-dot-dev/pen/WNpzRKd

마침 codepen에 좋은 예시가 있더군요. 예시를 보면 폰트 크기가 동일해도 폰트마다 고유 크기가 다를 수 있다는 것을 알 수 있습니다. 즉, 같은 64px이라고 해도 Robot 폰트랑 morospace 폰트는 크기가 다릅니다.

아래는 제가 직접 sans-serif와 HomemadeApple 폰트를 비교해본 결과입니다. 같은 크기라는게 믿기지 않을 정도로 큰 차이를 보여줍니다.

FOUT(Flash Of Unstyled Text)

근데 폰트마다 크기가 다른게 Layout Shift랑 직접적으로 무슨 연관이 있을까? 라며 의아해하실지도 모르겠습니다. FOUT 라는 개념에 익숙치 않으면 그럴 수 있습니다.

간단하게 실습을 통해 알아보도록 하겠습니다. 저는 HomemadeApple 이라는 폰트를 사용했고, 구글 폰트에서 다운받아서 사용했습니다. 폰트를 적용할 때는 css에서 @font-face를 사용하는 것이 일반적인 형태입니다. @font-face 사용법에 대해서는 찾아보면 쉽게 나오니 여기서는 설명하지 않겠습니다.

MDN 참고 : https://developer.mozilla.org/ko/docs/Web/CSS/@font-face

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <style type="text/css">
      @font-face {
        font-family: "HomemadeApple";
        font-style: normal;
        font-weight: 400;
        src: url("./font/HomemadeApple-Regular.ttf") format("truetype");
      }

      body {
        font-family: "HomemadeApple", sans-serif;
        font-size: 16px;
      }
    </style>

    <title>Font CLS</title>
  </head>
  <body>
    <h1>IMG LCP</h1>
  </body>
</html>

아, 참고로 요즘 브라우저와 컴퓨터 성능이 좋아서 제대로 안보일 수 있는데 이럴때는 개발자도구에서 네트워크 속도 조절을 할 수 있습니다.

이렇게 하고 나서 결과를 보면 FOUT가 뭔지 알 수 있습니다.

FOUT란 웹 폰트가 적용되기 전, 스타일이 적용되지 않은 기본 폰트가 먼저 적용되는 것을 말합니다. 이런 기본 폰트를 흔히 fallback font(폴백 폰트)라고 합니다. 그리고 나서 웹 폰트가 로딩이 완료되면 그제서야 swap이 일어나면서 제가 원하는 폰트로 보이게 됩니다. 이 과정에서 flash 현상(깜빡거리는 현상)이 나타나서 Flash Of Unstyled Text라고 합니다.

참고 : https://developer.chrome.com/blog/font-fallbacks/

다음은 fallback font에 대한 설명입니다. 기본 폰트가 아직 로드되지 않았거나 페이지 내용을 렌더링하는데 필요한 glyphs가 없을 때 사용되는 font face입니다. 아래 CSS는 sans-serif 글꼴이 Roboto의 font fallback으로 사용됨을 의미합니다.

font-family: "Roboto" , sans-serif;

이러한 fabllback font는 텍스트를 더 빨리 렌더링해서 페이지 내용을 더 빨리 읽을 수 있게 하는데 유용하지만, 레이아웃은 불안정합니다. 왜냐면 fallback font를 웹 폰트로 swap하기 때문입니다. swap만 하면 문제 없겠지만 폰트마다 자체 크기가 다르기 때문에 그냥 사용할 경우 레이이아웃은 불안정하게 됩니다.

그래서 아래에서 논의되는 새로운 API(size-adjust, ascent-override, descent-override, and line-gap-override)가 나오게 되었고 이를 통해 웹 폰트와 동일한 공간을 차지하는 폴백 폰트를 만들 수 있게 함으로써 이 문제를 줄이거나 제거할 수 있게 되었습니다.

FOIT(Flash Of Invisible Text)

참고로 말씀드리자면 모든 브라우저가 FOUT 방식을 따르는 것은 아닙니다. FOIT(Flash of Invisible Text)라고 해서 폰트가 준비되지 않은 경우 폴백 폰트를 사용하는 것이 아니라 아예 안보였다가 폰트가 로딩이 되면 보여주는 방식도 있습니다.

참고 : https://web.dev/i18n/ko/avoid-invisible-text/

사실 위에 내용도 최신인지 아닌지는 잘 모르겠습니다. 위 테스트에서는 크롬에서 테스트했는데 저 설명이랑 다른거 같네요... 버전 업 되면서 정책이 바뀐걸지도 모르겠습니다.

font-display 설정

그러면 브라우저마다 정해진 방식대로 FOUT/FOIT 를 따를 수 밖에 없을까요? 그건 아닙니다. font-display 설정을 통해 제가 원하는 방식대로 바뀔 수 있습니다.

흔히 FOUT로 할 것인지, FOIT로 할 것인지는 서비스 종류와 정책에 따라 다를 것입니다. 사용자에게 뭐라도 빨리 보여주고 싶다면 FOUT로 하면 될 것이고, 좀 늦게 보여주더라도 이쁜 폰트를 보여주겠다고 하면 FOIT로 하면 될 것입니다.

참고 : https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display
참고 : https://d2.naver.com/helloworld/4969726 (그림으로 잘 표현되어있습니다)

  • auto : 브라우저(user agent) 기본 동작을 따름
  • block : "short block period and an infinite swap period." - 3초 동안은 보이지 않는 텍스트를 렌더링를 하고 그 이후에는 fallback font를 보여줍니다. 폰트가 로드되면 바로 교체해줍니다.
  • swap : "extremely small block period and an infinite swap period." - 우선 100ms 동안 텍스트가 보이지 않고, 그 이후 fallback font을 보여주고 폰트가 로드되면 바로 교체해줍니다.
  • fallback : "extremely small block period and a short swap period." - 우선 100ms 동안 텍스트가 보이지 않고, 3초동안은 fallback font를 보여줍니다. 그 동안 폰트가 로드되면 바로 교체해줍니다. 하지만, 그 이상 넘어가면 더 이상 폰트 교체는 하지 않습니다. 대신 캐시에는 저장이 되어 나중에 다시 방문했을 때는 제대로 보일 것입니다.
  • optional : "an extremely small block period and no swap period." - 우선 100ms 동안 텍스트가 보이지 않고, 그 이후에는 fallback font를 보여줍니다. swap이 될지 말지는 모릅니다. 브라우저가 네트워크 상태를 파악해 웹 폰트 전환 여부를 결정한다고 합니다. (제일 뭔가 애매한 속성...)

Zero Layout Shift

방법1. How font metric overrides work

참고 : https://developer.chrome.com/blog/font-fallbacks/

Layout Shift를 줄이기 위한 방법은 2가지가 있다고 합니다. 첫번째는 폰트의 ascent, descent, line-gap 을 재정의하는 방법을 사용합니다.

  • ascent : baseline을 기준으로 위로 확장되는 가장 먼 거리를 측정
  • descent : baseline을 기준으로 아래로 확장되는 가장 먼 거리를 측정
  • line gap : 또는 leading이라고 불리며, 텍스트 줄 사이 간격을 측정

글꼴 메트릭 재정의는 웹 글꼴의 상승, 하강 및 선 간격과 일치하도록 폴백 글꼴의 상승, 하강 및 선 간격을 재정의하는 데 사용될 수 있습니다. 결과적으로 웹 글꼴과 조정된 폴백 글꼴은 항상 동일한 치수를 가질 수 있습니다.

정확한 계산법을 위해서는 font 메타데이터(폰트 테이블?)라는게 있는데 이걸 보고 수치를 확인한다음 계산법에 의해 계산을 해주면 된다고 합니다... 그것까지 좀... 알아보기는 힘들거 같아서 생략하겠습니다.

방법 2. How size-adjust works

size-adjust는 폰트 glyphs의 너비와 높이를 비례적으로 조정합니다. 예를 들어, 크기 조정: 200%는 글꼴 글리프를 원래 크기의 두 배로 조정하고, 크기 조정: 50%는 글꼴 글리프를 원래 크기의 절반으로 조정합니다.

대부분의 경우 웹 폰트와 일치하기 위해서는 폴백 폰트를 좁히거나 약간 넓힐 필요가 있다.

size-adjudct와 글꼴 메트릭 재정의를 결합하면 두 글꼴이 수평 및 수직으로 서로 일치하도록 만들 수 있다. 즉, 1번 방법과 2번 방법을 결합하면 폴백 폰트와 웹 폰트의 비율을 맞출 수 있다는 것이다.

저는 HomemadeApple를 폴백 폰트로 맞춰봤는데 몇번 하다보니 대략적으로 수치를 맞출 수 있었습니다.

      @font-face {
        font-family: "HomemadeApple";
        font-style: normal;
        font-weight: 400;
        size-adjust: 68.5%;
        ascent-override: 120.6633866%;
        descent-override: 60%;
        src: url("./font/HomemadeApple-Regular.ttf") format("truetype");
      }

Next.js에서의 Zero Layout Shift

This means you can optimally load web fonts with zero layout shift, thanks to the underlying CSS size-adjust property used. - next.js 공식문서

Next.js에서의 좋은 점은 여러가지 최적화를 기본적으로 해준다는 것입니다. 그 중에서 폰트 역시 최적화가 잘 되어있습니다. 위에서 봤던 size-adjust를 사용해서 zero layout shift를 자동으로 적용해준다고 하네요.

실제로 확인해보니 font-display는 기본적으로 swap을 사용하고 있고, 폴백 폰트를 size-adjust, ascent-override, descent-override, and line-gap-override를 사용해서 조정하고 있는 것을 알 수 있습니다.

@font-face {
    font-family: '__homemadeApple_6e6db2';
    src: url(/_next/static/media/72167529e4286421-s.p.ttf) format('truetype');
    font-display: swap;
}

@font-face {
    font-family: '__homemadeApple_Fallback_6e6db2';
    src: local("Arial");
    ascent-override: 105.55%;descent-override: 68.88%;line-gap-override: 1.43%;size-adjust: 122.78% }

정말로 layout shift가 1도 없는 zero layout shift인 것을 알 수 있습니다.

구체적인 로직 분석 : https://blog.mathpresso.com/how-next-font-works-8bb72c2bae39

해당 로직에 대한 분석은 콴다 개발 블로그에서 자세히 설명해놨습니다... 참고하시면 좋을 거 같습니다.

참고 : http://deploy-preview-15--upbeat-shirley-608546.netlify.app/perfect-ish-font-fallback

위 사이트도 아마 같은 방식으로 되어있지 않나 생각합니다. 저런 수치를 계산하는 로직이 있는거 같습니다.


마치면서

이번 시간을 통해 폰트에 대해 조금이나마 익숙해질 수 있었던 거 같습니다.

아, 그리고 위 내용도 물론 중요하지만 근본적으로는 폰트 사이즈를 줄이고 빠르게 로딩하는 것이 가장 좋은 방법입니다. @font-face 설정을 통해 최적화된 폰트를 먼저 사용하도록 하고, 필요없는 글자(문자)에 대한 폰트는 제거한 subset 폰트를 사용해서 용량을 줄이는게 좋을 거 같습니다. 또한, preload를 통해 폰트를 가급적 빨리 가져오는 것도 방법입니다.


참고 자료

profile
배움을 좋아합니다. 새로운 것을 좋아합니다.

0개의 댓글