웹폰트 최적화 하기

vnthf·2020년 1월 2일
24

웹폰트란?

폰트

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

웹폰트가 나온 이유

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

문제점.

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

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

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

ex) Dooray의 font-family

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


(크롬 개발자 도구 -> 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%정도 더 좋음

웹폰트 확장자 순서

  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");

같은 폰트라면 같은 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을 만들어서 활용

  • python을 활용한 subset 생성기 https://github.com/fonttools/fonttools
  • Google의 subset 활용
  • 파일 크기를 상당히 줄일 수 있음.
<link href="https://fonts.googleapis.com/css?family=Noto+Sans+KR&display=swap&subset=korean&text=담당업무" rel="stylesheet">

폰트 용량을 줄이는 법

  • 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

  • 흔히 말하는 브라우저의 깜빡임이다.
  • 폰트에 따라 자간, 높이에 따라 레이아웃이 변경될 수 있다. 이럴 경우 나쁜 사용자 경험을 겪게 된다.
    • 그럴 땐 font-size-adjust 속성이나 letter-spacing, line-height 등을 조정하여 최대한 적게 변해 보이도록 바꿔야 한다.
    • https://meowni.ca/font-style-matcher/
  • IE, edge

왜 이런 현상이 발생할까

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

둘 중에 뭐가 좋은 걸까?

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

FOIT

FOUT

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

font-display

  • @font-face의 속성

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

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

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

fallback

FOIT x

FOUT x

하지만,

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

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 observer | webfontloader |
| --- | --- | --- |
| 크기 | 5.83 KB | 12.2 KB |
| 특징 | 가볍고 스크롤 이벤트를 탐지해서 로딩하기 때문에 빠름 | 구글과 typekit이 공동으로 만들어서 둘의 지원이 잘되어 있음
동기와 비동기 방식 제공,
상태에 따른 클래스를 html 태그에 직접 추가해줌|
| api | promise로 직접구현 | fontface가 있으면 우선 사용 |
| event | load만 제공 | 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>

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 이내에는 페이지의 어떤 것도 보이지 않음

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이 있어야 두 번 다운로드하지 않음



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

data URI 사용

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

local storage 혹은 session storage

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

FOFT 방식

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

4개의 댓글

comment-user-thumbnail
2020년 6월 25일

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

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

감사합니다! ㅎㅎ

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

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

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

감사합니다

답글 달기