Frontend

페이지 로딩할 때 폰트 적용 지연 시간 줄이기

date
May 24, 2024
thumbnail
font-optimization.png
slug
web-font-optimiaztion
author
status
Public
tags
CSS
Javascript
Blog
summary
웹 폰트 최적화에 대해서
type
Post
category
Frontend
 
웹 페이지 초기 로딩 시 화면이 그려진 뒤 폰트가 적용되는 시간이 걸려 FOUT(Flash of unstyled text)가 발생하는 현상을 해결해보려 한다.
 
FOUC (Flash of unstyled content), FOUT(Flash of unstyled text)
FOUC (Flash of unstyled content), FOUT(Flash of unstyled text)
FOUC는 외부 CSS를 불러오기 전, 잠시 스타일이 적용되지 않은 웹 페이지가 나타나는 현상을 말한다. 스타일이 적용되지 않은 웹 페이지가 스타일이 적용된 웹 페이지로 변화하면서 화면이 깜빡여 보일 수 있다. FOUT는 지정한 폰트가 보여지지 않고 대체 기본 폰트로 보이고 있다가 뒤늦게 폰트가 적용되는 현상을 말한다.
필요한 폰트가 준비되지 않아서 일시적으로 텍스트 자체가 보이지 않는 경우도 발생하는데, 이는 FOIT(Flash of Invisible text)라고 한다.
 
 
폰트가 적용되지 않았다가 적용된다.
폰트가 적용되지 않았다가 적용된다.
 

웹 폰트가 로드되는 과정


웹 페이지가 로드되는 과정을 보면 브라우저가 텍스트를 기본 시스템 폰트로 먼저 렌더링 하고, 나중에 웹 폰트가 로드되면 이를 교체한다. 브라우저에서 특정 주소로 이동할 때의 과정을 간단히 살펴보자. 브라우저가 응답으로 HTML 파일을 요청했을 때 이후의 과정인 파싱과 렌더 과정을 요약했다.
 
  1. HTML 파싱
      • 브라우저는 HTML 문서를 위에서 아래 순으로 파싱한다.
      • HTML 요소를 DOM 트리로 변환한다.
  1. CSS 파싱
      • 브라우저는 CSS를 파싱하여 CSSOM 트리를 생성한다.
      • 여기서 `@font-face` 규칙을 만나면 웹 폰트 파일을 비동기적으로 요청한다.
  1. DOM과 CSSOM 결합
      • DOM과 CSSOM을 결합하여 각 요소의 시각적 특성을 포함하는 렌더 트리를 생성한다.
      • 이 때, 폰트 로딩 상태와 관계없이 렌더 트리가 생성된다.
  1. 레이아웃 계산
      • 렌더 트리의 각 요소의 크기와 위치를 계산한다.
      • 이 때, 지정된 폰트가 아직 로드되지 않은 경우 기본 폰트를 사용한다.
  1. 페인트 및 합성(Composition)
      • 렌더 트리를 기반으로 요소들을 실제로 화면에 그린다.
      • 이후 웹 폰트를 사용할 수 있게 되면 브라우저는 텍스트를 다시 렌더링하여 웹 폰트를 적용한다. 이 과정에서 FOUT 현상이 발생한다.
 
브라우저 렌더링 과정 중 CSS를 파싱하는 과정에서 웹 폰트를 비동기적으로 요청하고, 렌더 트리를 기반으로 레이아웃을 계산하고 페인트할 때 아직 웹 폰트가 로드되지 않았다면 기본 폰트를 먼저 사용한다. 이후 지정한 웹 폰트가 로드되면 스타일이 적용된 텍스트를 다시 렌더링하는 방식이다.
 
 
왜 FOUT가 발생하는 지 알고 났더니 어떻게 해야 할 지 감이 보인다.
 

웹 폰트 용량을 줄이자


먼저 웹 폰트 리소스를 로드하는 데 걸리는 시간을 줄이기 위해서 웹 폰트 용량을 줄인다. 웹 폰트의 용량을 확인해보자. cdn을 통한 웹 폰트의 경우에는 대부분 폰트 형식이 최적화되어 있지만, 폰트를 직접 정적 애셋으로 사용하여 로드하는 경우에는 폰트 파일을 최적화하는 것부터 시작할 수 있다.
 

폰트 형식(포맷)

  • TTF (TrueType Font)
    • 비교적 큰 용량
    • 대부분의 브라우저에서 지원
    • OTF와 유사하지만 기능이 더 적음
    • 애플사에서 개발
  • OTF (OpenType Font)
    • TTF와 비슷한 용량
    • 대부분의 브라우저에서 지원
    • TTF를 기반으로 더 많은 기능을 지원하는 포맷
  • EOT (Embedded OpenType)
    • 비교적 작음
    • 인터넷 익스플로러에서 주로 사용
    • 마이크로소프트사에서 웹에서 TTF/OTF 폰트를 사용하기 위해 개발한 포맷
  • WOFF (Web Open Font Format)
    • TTF/OFF보다 작은 용량
    • 대부분의 모던 브라우저에서 지원
    • 더 높은 압축률을 통해 용량을 줄여 웹에 최적화한 포맷
  • WOFF2 (Web Open Font Format 2)
    • WOFF보다 작음
    • 대부분의 최신 브라우저에서 지원
    • WOFF의 후속으로, 더 높은 압축률을 통해 용량을 더욱 줄인 포맷
 
WOFF는 TTF/OTF와 거의 동일하게 동작하지만 압축을 통해 더 작은 파일 크기를 가진다. 모던 웹 개발을 위해서는 WOFF에서 WOFF2 포맷의 폰트를 웹 개발에 사용해야 한다.
 
caniuse에서 폰트 포맷에 따른 브라우저 지원 현황을 확인해보자. 2012년 W3C에 권장 사항으로 등록된 이후 현재는 거의 모든 브라우저에서 woff를 지원하지만, 모던 브라우저들 중에서도 오래된 버전의 브라우저에서는 woff도 지원하지 않는다. woff2는 인터넷 익스플로러에서는 지원하지 않는다.브라우저는 지원하지 않는 폰트 포맷을 만나면 지원되는 다른 폰트 포맷을 찾아 사용한다.
@font-face { font-family: 'MyFont'; src: url('myfont.woff2') format('woff2'), url('myfont.woff') format('woff'), url('myfont.ttf') format('truetype'), url('myfont.eot'); /* EOT for older IE versions */ }
여러 포맷의 폰트를 준비해서 여러 브라우저에서 같은 폰트를 보여주게 하자.
브라우저 간의 폰트 호환성을 최대로 높이고 싶다면 위와 같이 여러 포맷의 폰트를 준비하는 방법이 있다. 대부분의 경우에는 woff2woff까지 준비하면 된다.
 

서브셋(subset) 사용하기

내 블로그를 보면 메인 페이지의 특정 글자 ‘카키디깅록’에만 특정 폰트 ‘VISTRO’를 적용하고 있다. 이를 위해 모든 글리프(Glyph: 자체, 자형)를 포함한 파일을 제공할 필요가 없다. 필요한 글리프만 적용한 서브셋 폰트 파일을 만들어서 사용할 수 있다.
 
특히나 한글 폰트는 영문 폰트에 비해 자음과 모음을 합쳐 굉장히 많은 글리프를 가지고 있기 때문에, 한글로 만들 수 있는 글자를 모두 포함한 폰트는 용량이 매우 크다. 꼭 서브셋팅(Subsetting)으로 커스텀 서브셋 폰트 파일을 사용하는 것을 권장한다.
 
Google Fonts는 폰트 서브셋을 제공한다.
Next.js에서는 Google Fonts를 자동으로 preload한다.
Google Fonts는 폰트 서브셋을 제공한다. Next.js에서는 Google Fonts를 자동으로 preload한다.
 

웹 폰트 리소스를 프리로드(preload)하기

HTML에 우선순위가 높은 리소스를 미리 로드하는 속성을 적용하게 되면 렌더링 패스 초기에 웹 폰트 요청을 보낼 수 있다. 이는 CSSOM을 생성하기 전에 요청하므로 더 빨리 웹 폰트 리소스에 대한 응답을 받을 수 있다. IE에서는 지원하지 않는다.
 
<link rel="preload" href="font.woff2" as="font" type="font/woff2" crossorigin="anonymous">
 

font-display 속성으로 웹 폰트 로딩 제어하기

웹 폰트 리소르를 미리 요청했다는 것은 브라우저 렌더링 초기 과정에서 웹 폰트를 사용할 가능성이 높아지는 것이지 이를 보장하지는 않는다. @font-face의 font-display 속성은 웹 폰트가 로드되는 동안의 동작을 제어할 수 있는 속성이다. 이는 모던 브라우저에서 지원하고 IE는 지원하지 않는다.
 
@font-face { font-family: 'MyWebFont'; src: url('webfont.woff2') format('woff2'); font-display: swap; /* 폰트 로딩이 완료될 때까지 기본 폰트 사용 후 즉시 교체 */ }
 
font-display 속성은 총 5가지 값을 가지고 있다.
  1. auto : 브라우저의 기본 동작에 따라 표시
  1. block : 웹 폰트가 로드될 때까지 폰트를 숨기고, 로드 후 적용한다.
  1. swap : 웹 폰트 로딩이 완료될 때까지는 기본 폰트를 사용하고, 로드가 완료되면 즉시 웹 폰트로 교체한다.
  1. fallback : 기본 폰트와 함께 웹 폰트도 함께 로드되지만, 웹 폰트가 로드되기 전에 기본 폰트로 텍스트를 표시한다. 웹 폰트 로드가 지연되더라도 사용자는 텍스트를 볼 수 있다. 대부분의 경우 auto와 같은 동작을 한다.
  1. optional : 웹 폰트가 로드되기 전 기본 폰트로 텍스트를 표시하며, 웹 폰트가 로드되면 적용한다. 그러나 브라우저가 웹 폰트를 로드하지 않아도 될 경우, 로드하지 않는다.
 
내용을 읽어봐서는 결국 block을 제외하면 대부분 같은 동작을 하는 것처럼 보인다. 내 상황에서 적합한 속성은 무엇일까? Chrome for Developers에서 상황에 따른 font-display 속성을 추천해주고 있다. 내용은 아래와 같다.
 
옵션
설명
block
글꼴에 짧은 차단 기간 (대부분의 경우 3초가 권장됨)과 무한 스왑 기간을 제공합니다. 즉, 글꼴이 로드되지 않으면 브라우저는 처음에 '보이지 않는' 텍스트를 표시하지만 로드되는 즉시 글꼴로 전환합니다. 이를 위해 브라우저는 선택한 글꼴과 비슷하지만 모든 글리프에 '잉크'가 포함되지 않은 익명의 글꼴을 생성합니다. 이 값은 페이지를 사용하기 위해 특정 서체로 텍스트를 렌더링해야 하는 경우에만 사용해야 합니다.
swap
글꼴에 0초의 차단 기간과 무한 스왑 기간을 제공합니다. 즉, 글꼴이 로드되지 않으면 브라우저에서 대체를 통해 즉시 텍스트를 그리지만 로드되는 즉시 글꼴로 전환합니다. 블록과 마찬가지로 이 값은 페이지에 특정 글꼴로 텍스트를 렌더링하는 것이 중요할 때만 사용해야 하지만 어떤 글꼴로 렌더링하더라도 여전히 올바른 메시지를 받게 됩니다. 적절한 대체를 사용하여 회사 이름을 표시하면 메시지가 전달되지만 결국에는 공식 서체를 사용하게 되므로 로고 텍스트는 swap에 적합한 후보입니다.
fallback
글꼴에 매우 짧은 차단 기간 (대부분의 경우 100ms 이하 권장)과 짧은 스왑 기간 (대부분의 경우 3초가 권장됨)을 제공합니다. 즉, 글꼴이 로드되지 않으면 처음에는 대체로 렌더링되지만 로드되는 즉시 글꼴이 전환됩니다. 그러나 시간이 너무 오래 지나면 페이지의 남은 전체 기간 동안 대체가 사용됩니다. fallback은 사용자가 가능한 한 빨리 읽기를 시작하길 원하고 새 글꼴이 로드될 때 텍스트를 이동함으로써 사용자 환경을 방해하지 않기를 원하는 본문 텍스트 등의 항목에 사용하기 좋습니다.
optional
글꼴에 매우 짧은 차단 기간 (대부분의 경우 100ms 이하 권장)과 0초 스왑 기간을 제공합니다. fallback과 마찬가지로 이는 글꼴 다운로드 기능이 '있으면 좋지만' 환경에는 중요하지 않은 경우에 적합합니다. optional 값은 글꼴 다운로드를 시작할지 여부를 브라우저에 맡기고, 사용자에게 가장 적합하다고 판단되는 항목에 따라 다운로드하지 않거나 낮은 우선순위로 실행할 수 있습니다. 이는 사용자의 연결 상태가 좋지 않고 글꼴을 아래로 내리면 리소스를 가장 잘 활용하는 방법이 아닐 수 있는 상황에서 유용할 수 있습니다.
 
FOUT가 발생하는 텍스트가 로고와 같은 역할을 하므로, 이 경우에 나는 swap 방식을 선택한다.
 

적절히 캐싱하기

폰트는 업데이트가 거의 되지 않는 정적 리소스이므로, 브라우저 캐시를 통해 캐싱하는 것이 권장되는 리소스다. 따라서 웹 브라우저는 기본적으로 폰트를 캐싱한다. 브라우저는 폰트 파일에 대한 HTTP 요청에서 캐시 헤더를 확인해서 캐싱된 폰트 파일이 최신인지 확인하고, 최신 버전이 아닐 때만 새로 요청한다.
 
로컬 폰트를 사용한다면 주로 서버에서 해당 폰트 파일을 제공하므로 백엔드에서 폰트 캐싱 전략을 사용한다. 웹 서버나 CDN에는 캐시 컨트롤 프록시가 설정되어 있어 폰트 파일에 대한 캐싱을 관리할 수 있다. 직접 HTTP 헤더를 설정하지 않고도 캐싱을 제어할 수 있다.
 
로컬 폰트를 사용하지 않고 외부 서버에서 폰트 파일을 제공한다면 호스팅 서비스의 캐시 컨트롤을 적절히 활용하고, CDN을 사용한다면 CDN의 캐시 제어 기능으로 캐시 헤더를 설정하고, HTTP 응답 헤더를 설정하여 캐시 헤더를 설정해야 한다.
 

로컬 폰트를 사용해야 하는 경우

외부 서버에서 폰트 리소스를 받아와 사용하는 경우가 아니라 웹 페이지의 정적 리소스로 폰트를 제공하는 방법은 어떨 때 사용해야 할까?
 
  1. 웹 페이지 초기 로딩 속도가 중요할 때
    1. 초기 로딩 속도가 중요한 웹 페이지에서는 로컬 폰트를 사용해서 서버 통신에 따른 지연을 최소화하자
  1. 디자인 요구 사항에서 정확한 표현이 필요할 때
    1. 웹 페이지에서 정확한 스타일 및 렌더링을 보장해야 할 때는 로컬 폰트를 사용하자
  1. 오프라인 사용 및 네트워크 연결이 불안정한 환경
    1. 오프라인에서도 동작해야 하거나 네트워크 연결이 불안정한 환경에서도 같은 화면을 보여주어야 한다면 로컬 폰트를 사용하자
  1. 호환성 문제
    1. 특정 브라우저나 기기에서 외부 서버 폰트 리소스가 제대로 렌더링되지 않을 수 있을 때는 로컬 폰트를 사용해서 호환성 문제를 해결하자
 

배운 내용을 토대로 문제 해결해보기


현재 조건

  • ‘카키디깅록’이라는 텍스트에만 ‘Vitro Inspire’라는 폰트가 사용된다.
  • /public/fonts 디렉터리에 정적 리소스로 빌드 시 포함되고 있다. → 로컬 폰트
  • 네트워크 탭에서 확인한 결과 폰트 리소스 중 가장 큰 용량을 차지하고 있다.
    • notion image
 

서브셋을 만들자

모던하고 심플한 @font-face 생성 및 변환 서비스다. 온라인에 폰트를 업로드하면 ttf, woff, woff2, svg, eot의 폰트 포맷으로의 변환, 특정한 글자 혹은 유니코드 범위로 서브셋팅을 도와준다.
 
notion image
  • Characters에 ‘카키디깅록’을 직접 입력해주고 woff, woff2를 포함하여 서브셋팅했다.
  • 파일을 다운로드 해보면 1KB의 woff2 폰트, 2KB의 woff 폰트와 데모 등이 포함되어 있다.
 

적용해보기

변환한 파일을 public/fonts 디렉터리에 넣고, styles/fonts.css에서 @font-face를 지정한다.
@font-face { font-family: "VitroInspire-Subset"; src: url("/fonts/subset-VITROINSPIREOTF-Regular.woff2") format("woff2"), url("/fonts/subset-VITROINSPIREOTF-Regular.woff") format("woff"), url ("/fonts/VITRO_INSPIRE_OTF.otf") format("opentype"); font-display: swap; font-weight: normal; font-style: normal; }
 
‘카키디깅록’이라는 텍스트를 렌더링하는 컴포넌트에서 새 폰트로 지정한다.
<div className="flex flex-col justify-center items-center"> <h1 className={`overline font-['VitroInspire-Subset'] text-[72px] text-transparent bg-clip-text bg-gradient-to-r from-sky-600 to-sky-800 drop-shadow-2xl dark:from-gray-100 dark:to-sky-500`} > 카키디깅록 </h1> //...
  • tailwind css를 사용하고 있다.
 
폰트 파일을 preload 하도록 지정한다.
import Document, { Html, Head, Main, NextScript } from "next/document" class MyDocument extends Document { render() { return ( <Html lang={CONFIG.lang}> <Head> // ... <link rel="preload" href="/fonts/subset-VITROINSPIREOTF-Regular.woff2" as="font" type="font/woff2" /> <link rel="preload" href="/fonts/subset-VITROINSPIREOTF-Regular.woff" as="font" type="font/woff" /> // ...
  • next.js의 page router를 사용하고 있으므로 pages/_document.tsx 파일 내부에 적용한다.
 
브라우저 호환성을 위해 font-face에 한 폰트의 여러 포맷을 지정해주었다.
이 경우 폰트의 모든 포맷 파일을 preload하는 것이 좋을까?
브라우저 호환성을 위해 font-face에 한 폰트의 여러 포맷을 지정해주었다. 이 경우 폰트의 모든 포맷 파일을 preload하는 것이 좋을까?
  • preload는 브라우저에게 리소스를 미리 다운로드하라고 지시한다.
  • 즉, 리소스를 요청하지 않은 경우에도 해당 리소스를 미리 다운로드하게 한다.
  • 여러 포맷의 폰트를 preload하면 브라우저가 최적의 포맷을 선택할 수 있게 해주지만, 오히려 preload하는 리소스의 용량이 높아지면 초기 로딩 속도에 부정적인 영향을 줄 수 도 있다.
  • 이 밸런스를 잘 조절해서 최적의 선택을 해야 한다.
이미 서브셋팅을 통해 폰트 파일 크기를 1KB, 2KB로 줄여주었기 때문에 큰 고민없이 woff, woff2 폰트를 preload하게 설정했다.
 
 
notion image
notion image
  • 폰트 리소스를 더 빠르게 요청하는 만큼 더 빨리 다운로드된다.
  • 더불어 굉장히 압축된 용량으로 요청하는 만큼 더 빨리 다운로드된다.
 
 

정적 폰트가 아닌 가변 폰트를 사용하자


가변 폰트란?

가변 폰트(Variable Fonts)는 기존 정적인 폰트가 너비, 두께, 기울기 등에 따라 각각의 파일을 모두 따로 디자인하여 여러 파일로 사용하던 것과 달리, 한 글꼴 파일에 디자이너가 설정한 각 속성의 축을 만들어 속성마다 임의의 값을 적용하면 그에 맞는 폰트 모양이 만들어지는 글꼴이다. 즉, 하나의 글꼴 파일로 무한한 모양을 만들 수 있는 차세대 글꼴 포맷이라고 할 수 있다. Adobe, Apple, Google, Microsoft가 협력하여 개발하였으며 2016년 9월 14일 OpenType 1.8에서 발표되었다.
 

가변 폰트의 장점

  • 웹 성능 향상
  • 반응형 타이포그래피
  • 효율적인 리소스 활용
 

가변 폰트 다루기

웹에서는 CSS의 font-variation-settings 속성을 활용해 각종 축 값을 지정할 수 있다.
 
축의 종류
  • Weight: wght, 문자의 두께 → font-weight 속성
  • Width: wdth, 문자의 너비 → font-stretch 속성
  • Italic: ital, 문자의 이탤릭 여부 → font-style의 italic
  • Slant: slnt, 문자의 기울기 → font-style의 oblique와 각도
  • Optical size: opsz, 글자의 크기에 따라 적용되는 두께 → font-optical-sizing 속성
 
.title { font-variation-settings: 'wght' 800, 'wdth' 115; }
 
 
 

References