React

[React/리액트] 웹 폰트 로딩 최적화하기: preload와 swap 적용하기

solfa 2025. 1. 8. 15:49

문제 상황

개발을 진행하면서 눈에 거슬릴 정도로 사용자 경험을 해치는 부분이 하나 있었다. 바로 폰트이다.

 

영상을 보면 폰트가 깜빡인다는 문제를 확인할 수 있다. 이게 바로 FOUT(Flash of Unstyled Text)나 FOIT(Flash of Invisible Text) 현상인데 

 

FOUT(Flash of Unstyled Text)나 FOIT(Flash of Invisible Text)이란?

FOUT (Flash of Unstyled Text)

 

  • 폰트 로딩 전에 기본 폰트로 텍스트 표시
  • 폰트 로드 후 깜빡이면서 폰트 교체

 

FOIT (Flash of Invisible Text)

  • 폰트 로딩 전까지 텍스트 자체를 숨김
  • 폰트 로드 후 텍스트가 갑자기 나타남

이런 문제라고 설명할 수 있다. 나는 이를 개선하기 위해 폰트 로딩 최적화를 진행했다.


웹 폰트 최적화 방법들

웹 폰트 최적화에는 여러 가지 방법이 있다.

1. Preload 적용

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

 

이렇게 index.html에 폰트를 미리 다운받겠다 명시를 하는 작업이다. 이 방법은 구현이 매우 간단하고 쉽게 다른 리소스보다 먼저 폰트를 로드할 수 있게 하고 브라우저 지원이 좋다는 장점이 있다. 하지만 매번 새로운 요청이 발생하기도 하고 폰트 외의 다른 중요 리소스의 로딩이 밀릴 수 있다는 단점을 가지고 있다.

2. Service Worker를 통한 캐싱

// service-worker.js
self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open('font-cache').then((cache) => {
      return cache.addAll([
        '/fonts/Pretendard-Regular.woff2',
        '/fonts/Pretendard-Medium.woff2',
        '/fonts/Pretendard-Bold.woff2'
      ]);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

 

 

다음은 서비스 워커를 활용한 폰트 캐싱이다.

서비스 워커라는 클라 <-> 서버 사이에 또다른 매개체를 만들어서 곳에 폰트 캐싱해서 저장 요런 느낌이다.

 

폰트 외에도 캐싱을 해두면 오프라인 상황에서 대응이 매우 좋겠다고 느꼈지만 단순 폰트 하나 저장을 위한 워커 생성은 비효율적이라 판단해서 진행하지는 않았다.

3. font-display 속성 활용

@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont.woff2') format('woff2');
  font-display: swap; /* 또는 block, fallback, optional */
}

 

폰트가 보여질 때 어떤 전략을 사용하느냐에 따른 방법이다. 옵션은 다음과 같다.


- auto: 브라우저 기본 전략 사용
- block: 3초간 보이지 않다가 폰트 적용 (FOIT)
- swap: 시스템 폰트 먼저 보여주고 교체 (FOUT)
- fallback: 100ms 동안 안 보였다가 시스템 폰트로 교체, 폰트 로드되면 교체
- optional: 100ms 동안 안 보였다가 네트워크 상태에 따라 웹폰트 적용 여부 결정

 

난 이 중 swap을 선택했다. 왜냐하면 1. 컨텐츠를 바로 볼 수 있음 (UX 향상) 2. 폰트 교체가 자연스러움 3. 다른 옵션들에 비해 깜빡임이 덜함 보통 swap을 많이 사용하기도 한다.

 

이런 식으로 하면 브라우저가

  • woff2를 우선 시도 (더 작은 파일 크기)
  • woff2를 지원하지 않으면 woff 사용

이렇게 폰트가 로드되기 전까지 1안을 보여주다가, 로드가 완료되면 2안으로 교체되는 swap 형식이다!



4. 폰트 서브셋팅

@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont-subset.woff2') format('woff2');
}


필요한 글자만 추출해서 폰트 파일 크기를 줄이는 방법이다. 한글의 경우 자주 사용하는 글자만 선별하는 방식! 예를 들어 우리 서비스는 독서 공간이니까 도서관, 북카페 이런 단어를 추출하는 식으로!

5. 최신 폰트 포맷 사용

@font-face {
  font-family: 'MyFont';
  src: url('/fonts/MyFont.woff2') format('woff2'),
       url('/fonts/MyFont.woff') format('woff');
}


WOFF2가 가장 압축률이 좋음
- 브라우저 호환성을 위해 WOFF도 함께 제공

6. 로컬 폰트 우선 사용

@font-face {
  font-family: 'MyFont';
  src: local('MyFont'),
       url('/fonts/MyFont.woff2') format('woff2');
}


사용자 시스템에 설치된 폰트 먼저 사용한다. swap을 쓴다면 굳이!


최종 선택: Preload + Swap + 로컬 폰트

여러 방법 중 이 조합을 선택한 이유는 구현이 상대적으로 단순하고 관리 포인트가 적다는게 좋았다. 이 정도만 해도 효과는 충분히 좋다고 느꼈고 브라우저 호환성에서도 문제가 없었다.

 

preload 사용 후 최종 화면 모습이다.

 

서비스 워커가 한 번 캐싱되면 웹의 캐시 스토리지에 저장돼서 로딩 측면에서는 매우 굿!!! 이었는데, 그리고 서비스 워커를 만드는 것 역시 어렵지 않았지만! 관리가 번거로워서 쓰지 않기로 했다.

  1. 이전 캐시 삭제나 캐시 관리가 주기적으로 이루어져야 함
  2. 개발환경과 배포 환경 사이 관리가 어려움
    -> 서비스 워커 하나에 서버 주소 하나를 매치하는 편이라 지금 localhost에서는 잘 되지만 배포해서 주소가 바뀌었을 때 오류가 나기 쉬움

이런 이유 때문에 preload 사용해도 충분할 같다고 생각을 했다. Service Worker도 매력적인 옵션이었지만, 현재 프로젝트 규모와 팀 상황을 고려했을 때 유지보수 비용이 더 클 것으로 판단했다. 그리고 이미 유의미한 결과를 얻어서! 근데 한 번 해보고 싶긴 하다.

728x90