웹폰트 최적화 하기

vnthf·2020년 1월 2일
89

웹폰트란?

폰트

  1. web safe font - 일반적으로 시스템에 설치된 폰트. 다운로드 없이 사용자에게 의도대로 표현이 할 수 있음. (Arial, Helvetica 등)
  2. web font - 설치되어 있지 않아서 브라우저에서 다운로드해야 하는 폰트

웹폰트가 나온 이유

  • 시스템에 폰트가 없다면 폰트를 보여줄 수 없음
  • 디자이너의 의도대로 표현할 수 없음.
  • 가독성이 떨어짐
  • 한국어는 특히, web safe font로는 한계가 있음.

문제점.

  • 웹폰트 다운로드 시간만큼 rendering이 느려진다.
  • 한글 폰트는 용량이 크다.
    font-time.png

웹에서 폰트는 어떻게 적용할까

  • font-family 속성으로 적용
  • 사이트 전체 글꼴은 body에 적용하고 예외적으로 추가
  • 앞에 선언되어있는 글꼴부터 표현할 수 있는 순서대로 적용
  • font-family에 선언된 글꼴들이 모두 설치되어 있지 않다면 기본글꼴을 보여줌.
    • 윈도우: 맑은고딕, : apple 산돌 고딕

ex) Dooray의 font-family

body {
    font-family: "Nanum Gothic", "Noto Sans JP", sans-serif, Lucida Sans Unicode, arial;
}

image.png
(크롬 개발자 도구 -> Element -> computed 확인 )

@font-face

@font-face {
    font-family: 'Nanum Gothic';
    font-style: normal;
    font-weight: 400;
    src: url(/static_fonts/NanumGothic-Regular.eot), 
        url(/static_fonts/NanumGothic-Regular.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-Regular.woff) format("woff"),
        url(/static_fonts/NanumGothic-Regular.ttf) format("truetype");
}
  • css selector와 같이 쓰이지 않고 따로 선언되어야 함.
  • 브라우저가 해당 폰트를 렌더링해야 하는 시점에 동작.
  • 같은 폰트일 경우 하나의 @font-face룰만 선언

웹폰트 확장자 종류

  • 주로 4가지 형식이 쓰임.
  • EOT: IE8 이하일 경우
  • TTF: 구형 안드로이드버전(4.4)에서 필요.
  • WOFF: 대부분의 모던 브라우저에서 지원
  • WOFF2: WOFF보다 압축률이 30%정도 더 좋음

image.png

웹폰트 확장자 순서

  1. woff2를 가장 앞에
    • 브라우저는 선언된 순서대로 지원 가능한 파일 형식을 다운로드받기 때문에 압축률이 가장 좋은 woff2를 먼저 선언
  2. format()은 반드시 써야한다.
    • 쓰지 않으면 브라우저는 지원 가능한 파일 형식이 나올 때까지 순서대로 다운받음
  3. IE8이하를 지원해야 할 경우 eot가 가장 먼저
    • IE는 format을 읽지 못하기 때문에 가장 앞에 선언되어야 함.
   src: url(/static_fonts/NanumGothic-Regular.eot), 
        url(/static_fonts/NanumGothic-Regular.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-Regular.woff) format("woff"),
        url(/static_fonts/NanumGothic-Regular.ttf) format("truetype");

local 문법을 쓰자

  • 위처럼 선언하면 시스템에 폰트 유무와 관계없이 무조건 다운로드받게 됨.
    • 불필요한 리소스 요청
  • local 문법을 앞에 선언해주면 시스템에 설치되어 있다면 리소스를 요청하지 않음
   src: local('Nanum-Gothic'), 
        url(/static_fonts/NanumGothic-Regular.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-Regular.woff) format("woff");

image.png

같은 폰트라면 같은 font-family로

@font-face {
    font-family: 'Nanum Gothic'; 
    font-style: normal;
    font-weight: 400;
    src: url(/static_fonts/NanumGothic-Regular.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-Regular.woff) format("woff"),
        url(/static_fonts/NanumGothic-Regular.ttf) format("truetype");
}

@font-face {
    font-family: 'Nanum Gothic'; /* Nanum Gothic Bold x */
    font-style: normal;
    font-weight: 700;
    src: url(/static_fonts/NanumGothic-Bold.woff2) format("woff2"),
        url(/static_fonts/NanumGothic-Bold.woff) format("woff"), 
        url(/static_fonts/NanumGothic-Bold.ttf) format("truetype");
}

@font-face {
    font-family: 'Nanum Gothic'; /* Nanum Gothic ExtraBold x */
    font-style: normal;
    font-weight: 800;
    src: url(/static_fonts/NanumGothic-ExtraBold.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-ExtraBold.woff) format("woff"), 
        url(/static_fonts/NanumGothic-ExtraBold.ttf) format("truetype");
}

@font-face {
    font-family: 'Nanum Gothic'; /* Nanum Gothic italic x */
    font-style: italic;
    font-weight: 400;
    src: url(/static_fonts/NanumGothic-Regular-italic.woff2) format("woff2"), 
        url(/static_fonts/NanumGothic-Regular-italic.woff) format("woff"), 
        url(/static_fonts/NanumGothic-Regular-italic.ttf) format("truetype");
}
  • 그렇다고 모든 범위의 서체를 쪼갤 필요는 없음
  • 꼭 필요한 굵기와 스타일의 폰트만 다운로드 받도록

subset의 활용

unicode-range

@font-face {
  font-family: 'Nanum Gothic';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.10.woff2) format('woff2');
  unicode-range: U+d1b4, U+d1b6-d1f3, U+d1f5-d22b, U+d22e-d22f, U+d231-d233, U+d235-d23b, U+d23d-d240, U+d242-d256;
}
/* [11] */
@font-face {
  font-family: 'Nanum Gothic';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: local('NanumGothic'), url(https://fonts.gstatic.com/s/nanumgothic/v17/PN_3Rfi-oW3hYwmKDpxS7F_z-7rJxHVIsPV5MbNO2rV2_va-Nv6p.11.woff2) format('woff2');
  unicode-range: U+d105-d12f, U+d132-d133, U+d135-d137, U+d139-d13f, U+d141-d142, U+d144, U+d146-d14b, U+d14e-d14f, U+d151-d153, U+d155-d15b, U+d15e-d187, U+d189-d19f, U+d1a2-d1a3, U+d1a5-d1a7, U+d1a9-d1af, U+d1b2-d1b3;
}

출처: 구글의 나눔고딕 cdn (https://fonts.googleapis.com/css?family=Nanum+Gothic&display=swap)

  • font-face의 unicode-range 속성
  • 지원가능한 unicode 범위를 정해놓고 해당 속성에 일치하는 글자를 렌더링할 때 다운로드를 함.
  • 다국어를 지원하는 사이트의 경우 유용
    • 나눔고딕 -> 한글의 unicode range, notosans JP -> 일본어 unicode range
  • unicode-range는 subset font와 함께 쓰여야 최적화
    • unicode-range를 지원하지 않는 구형 브라우저의 경우는 한 파일로 다운로드 받아야 함

subset을 만들어서 활용

<link href="https://fonts.googleapis.com/css?family=Noto+Sans+KR&display=swap&subset=korean&text=담당업무" rel="stylesheet">

image.png

폰트 용량을 줄이는 법

  • subset 활용
  • EOT나 TTF는 직접 호스팅하는 경우 GZIP 설정
  • 폰트 자체는 바뀔 일이 거의 없음으로 max-age를 길게 잡고 캐싱해도 됨.
  • 폰트는 대표폰트 한두 개만 사용.

리소스 순서를 최적화

FOIT, FOUT

  • 웹폰트의 문제점 중 하나.


출처: https://www.malthemilthers.com/font-loading-strategy-acceptable-flash-of-invisible-text/

FOIT (Flash of Invisible Text)

  • 웹폰트가 로드될 때까지 텍스트를 렌더링 하지 않다가 로드가 된 이후에 텍스트를 보여주는 동작이다.

FOUT (Flash of Unstyled Text)

  • 웹폰트가 로드될때까지 시스템의 기본 폰트를 보여주고 이후 reflow 해서 글꼴을 대체하는 방식

FOIT, FOUT의 특징

FOIT

  • 폰트가 로딩되지 않으면 웹페이지의 블락을 가져온다.
  • 모던브라우저의 경우는 기다리는 제한 시간이 있다.
    • 3초 동안 기다리고 그 후에는 시스템 기본 폰트를 보여준다.
    • 크롬의 경우 아래와 같은 메시지가 뜬다
  • chrome, safari, firefox

FOUT

  • 흔히 말하는 브라우저의 깜빡임이다.
  • 폰트에 따라 자간, 높이에 따라 레이아웃이 변경될 수 있다. 이럴 경우 나쁜 사용자 경험을 겪게 된다.
  • IE, edge

왜 이런 현상이 발생할까

  • 폰트 리소스를 받는 시점이 글꼴을 렌더링하는 시점과 경합이 일어나기 때문.

둘 중에 뭐가 좋은 걸까?

  • FOIT, FOUT 둘 다 최소화 하는 게 좋음
  • 사용자와 개발자 입장에서는 최대 3초간 콘텐츠가 블락되는것은 좋지 않은 일임.
    • 일단은 텍스트라도 먼저 보이는 게..
  • 디자이너는 FOUT가 정말 신경 쓰일 수 있음.
    • 특히 빨리 시간내에 폰트가 다운받아질 경우 페이지 reflow가 신경 쓰임

FOIT

image.png

image.png

FOUT

image.png

FOIT를 방지하고 FOUT를 적용하되 최소화 하는 방법으로 설정

font-display

  • @font-face의 속성

auto - 브라우저의 기본동작에 맡기는 방식이다.
block - FOIT 즉, 타임아웃까지 텍스트를 보여주지 않음
swap - 응답이 올 때까지 무한정 기다리고 그 전까진 바로 기본폰트를 보여준다. 꼭 적용해야만 하는 중요폰트일 경우에 쓸 수 있다.
fallback - 100ms 내외의 시간 동안만 block을 하고 기본폰트를 보여준다. 응답이 오면 해당 폰트로 swap 하지만 짧은 시간(3s)만 기다린다.
optional - 100ms 내외의 시간 동안만 block을 하고 기본폰트를 보여준다. 그 후에는 대체하지 않는다. => 폰트가 상관이 없을 때

fallback이 위에서 말한 FOIT를 방지하고 FOUT를 최소화하는 방법이다.

image.png

  • IE와 edge를 빼고는 모두 지원.
    • IE와 edge는 어차피 FOUT라 FOIT를 방지 할 수 있음

fallback

FOIT x
image.png
FOUT x
image.png

하지만,

  • CSS로는 설정에 한계가 있음
    • swap time과 block time을 내 마음대로 설정할 수도 없고
    • 폰트 상태에 따른 이벤트를 받을 수도 없음.
  • 100ms는 한글 폰트에게는 너무 짧은 시간임

image.png

Javascript를 이용한 방법

font face

FontFace object가 있지만 모든 브라우저 지원이 아님.

async function loadFonts() {
    const font = new FontFace('myfont', 'url(myfont.woff)');
    // wait for font to be loaded
    await font.load();
    // add font to document
    document.fonts.add(font);
    // enable font with CSS class
    document.body.classList.add('fonts-loaded');
}

출처: https://developer.mozilla.org/en-US/docs/Web/API/FontFace/FontFace

javascript를 이용한 방법

font face observer, webfontloader

font face observerwebfontloader
크기5.83 KB12.2 KB
특징가볍고 스크롤 이벤트를 탐지해서 로딩하기 때문에 빠름구글과 typekit이 공동으로 만들어서 둘의 지원이 잘되어 있음
동기와 비동기 방식 제공,
상태에 따른 클래스를 html 태그에 직접 추가해줌
apipromise로 직접구현fontface가 있으면 우선 사용
eventload만 제공loading, active, inactive 등을 제공

class 추가를 통한 FOUT

  • body에 fallback 폰트를 설정하고 폰트가 로딩이 되면 class를 변경한다.
  • FOUT 발생
body {
    font-family: sans-serif, Lucida Sans Unicode, arial;
}

.fonts-loaded body {
     font-family: 'Nanum Gothic', sans-serif, Lucida Sans Unicode, arial;
}
<script>
var font = new FontFaceObserver('Nanum Gothic');
font.load().then(function() {
  document.documentElement.classList.add('fonts-loaded');
});
</script>

최적화전략x.gif

FOUT.gif

opacity 설정을 통한 FOUT 최소화


.fonts-loaded body {
     font-family: 'Nanum Gothic', sans-serif, Lucida Sans Unicode, arial;
}

.blocking-time {
    opacity: 0;
}

.fonts-loaded.blocking-time {
    opacity: 1;
}
<script>
document.documentElement.classList.add('blocking-time');
setTimeout(function() {
    document.documentElement.classList.remove('blocking-time');
}, 400)
var font = new FontFaceObserver('Nanum Gothic');
font.load(null, 3000).then(function() {
  document.documentElement.classList.add('fonts-loaded');
});

</script>
  • 400ms 이내에 한글 폰트가 전송이 되었을 경우 깜빡임 없이 바로 적용된 폰트가 보임.
  • 400ms 이내에는 페이지의 어떤 것도 보이지 않음

최적화.gif

sessionstorage 전략

  • 이미 한번 받은 폰트는 브라우저에 캐시.
  • 다음번 방문 때 불필요한 탐색을 줄여줌.
<script>
if (sessionStorage.fontsLoaded) {
    var html = document.documentElement;
    html.classList.add("fonts-loaded")
)} else {
    document.documentElement.classList.add('blocking-time');
    setTimeout(function() {
        document.documentElement.classList.remove('blocking-time');
    }, 400)
    var font = new FontFaceObserver('Nanum Gothic');
    font.load(null, 3000).then(function() {
      document.documentElement.classList.add('fonts-loaded');
      sessionStorage.fontsLoaded = true;
    });
}
</script>

preload

 <link rel="preload" href="/static_fonts/NanumGothic-Regular.woff2" as="font" type="font/woff2" crossorigin="anonymous">
  • preload를 이용해서 폰트를 받으면 다른 어떤 것보다 먼저 리소스를 요청
    • Dom Contented loaded 시점 이전에 렌더링이 됨
    • FOUT와 FOIT가 없어짐
    • 그만큼 렌더링이 느려짐
  • 사용 여부와 관계없이 무조건 리소스를 받음
    • 화면에 꼭 필수적인 폰트를 로딩해야 할 때 사용.
  • as와 crossorigin이 있어야 두 번 다운로드하지 않음

image.png
preload.gif
image.png

쓰지 말았으면 하는 최적화 방법

data URI 사용

  • font를 data uri를 사용해서 받아오는 방법.
  • foit와 fout가 방지됨
  • 하지만 CSS 파싱이 블락 됨. 렌더링이 상당히 지연.
  • 한글 폰트의 경우는 크기가 커서 속도도 오히려 더 느림

local storage 혹은 session storage

  • 스토리지에 data uri 방식으로 저장
  • 비동기라 블락은 되지 않지만 스토리지 용량 문제, 성능 문제가 있음
    • 구글에서도 권유하고 있지 않음
  • 브라우저 캐시 사용
    • 캐싱이 자주 풀리는 곳은 적용할만함.

FOFT 방식

  • Flash of Faux Text의 약자
  • 스크립트를 통해 조작
    • subset을 쪼개서 주요 폰트를 먼저 받아 볼드, 이태릭등의 스타일에는 가짜 폰트를 보여주고 진짜로 대체하는 방식
  • 로딩은 빨리할 수 있지만 두 번의 깜빡임이 일어남
  • font-family를 스타일별로 네이밍을 쪼개야 해서 관리가 힘듦.
    • 메인 페이지 같은 곳에 적용
profile
6년차 프런트엔지니어

6개의 댓글

comment-user-thumbnail
2020년 6월 25일

감사합니다. 많은 도움이 되었습니다!

답글 달기
comment-user-thumbnail
2020년 6월 29일

감사합니다! ㅎㅎ

답글 달기
comment-user-thumbnail
2020년 10월 9일

정말 상세한 글이네요! 잘 읽었습니다. 감사합니다 (_ _)

답글 달기
comment-user-thumbnail
2020년 10월 18일

감사합니다

답글 달기
comment-user-thumbnail
2021년 3월 24일

좋은 정보 감사합니다

답글 달기
comment-user-thumbnail
2022년 5월 20일

정말 잘 봤습니다.

답글 달기