0. FOIT과 FOUT
토이프로젝트로 지뢰찾기를 만들던 중 배포 후 폰트가 늦게 적용되는 현상이 나타났다.
폰트가 늦게 적용되는 현상에는 FOIT(Flash of Invisible Text), FOUT(Flash of Unstyled Text)가 있는데,
FOIT은 브라우저가 폰트를 다운로드하기 전까지 글자가 보이지 않는 현상이고,
FOUT은 브라우저가 폰트를 다운로드하기 전까지 폰트 스타일이 적용되지 않는 현상이다.
나의 경우 FOUT 현상이 나타났다.
폰트가 늦게 적용되는 현상이 일어나는 것은 브라우저의 동작에서 원인을 찾을 수 있다.
아래 그림은 브라우저가 페이지를 렌더링할 때 정보를 요청하는 순서를 나타낸다.
T0: 브라우저가 HTML 문서 요청
T1: DOM 구성, 브라우저가 CSS, JS 및 기타 리소스 요청
T2: 모든 CSS를 수신한 후 CSSOM 구성, CSSOM과 DOM을 결합하여 렌더링 트리를 구성한다.
이 시점에서 폰트 리소스를 요청한다.
T3: 페이지를 화면에 그린다. 이때 폰트 리소스를 사용할 수 없는 상태라면 폰트가 뒤늦게 적용되는 현상이 나타난다.
1. 서브셋 폰트 적용하기
서브셋 폰트(subset font)는 폰트 파일에서 불필요한 글자를 제거하고 사용할 글자만 남겨둔 폰트다.
아시다시피 지뢰찾기 앱의 경우 많은 글자가 필요하지 않다. 숫자와 몇가지 알파벳만 필요할 뿐이다.
내가 적용한 폰트는 영어와 숫자 뿐 아니라 한글까지 모두 포함된 폰트까지 있었기 때문에 불필요한 큰 용량을 차지한다고 생각했다.
그래서 서브셋폰트 메이커를 사용하여 서브셋 폰트를 만들었다.
일본어만 지원이 되기 때문에 조금 놀랐는데 다행히 네이버 개발자 선생님들께서 사용법을 쉽게 알려주셔서 사용할 수 있었다.
사용할 폰트를 불러오고 사용할 문자셋을 작성하여 변환하면 된다.
woff포맷 변환여부를 체크해도 ttf 파일로 생성되어서 생성된 서브셋폰트를 convertio라는 페이지에서 용량이 더 작은 woff 파일로 변환하였다.
하지만 서브셋폰트로 적용하고 파일 확장자를 woff로 변환한 후에도 여전히,, FOUT 현상이 나타났다.
2. font preload 기능 사용하기
지뢰찾기 앱은 단일 페이지 웹이다. 따라서 한 페이지에서 모든 폰트를 프리로드하면 된다.
그래서 아래와 같이 모든 폰트를 프리로드하는 코드를 작성하였다.
<link rel="preload" as="font" type="font/woff" />
import localFont from "next/font/local";
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<head>
<link rel="preload" as="font" type="font/woff" />
</head>
<body className={inter.className}>{children}</body>
</html>
);
}
최초에 페이지를 렌더링했을 때 알 수없는 이름의 폰트가 프리로딩 되긴 했지만
각 폰트가 필요한 컴포넌트가 렌더링될 때가 되어서야 다시 폰트가 다운로드 되는 것을 확인했다.
확인해보니 Safari에서는 프리로드가 의도적으로 동작하지 않는다는 것을 알게되었다.
폰트에 대한 요청은 CORS 요청인데 Safari에서는 폰트 프리로드 요청에 대해 CORS 요청을 하지 않는다는 것이다.
이는 폰트의 소유권을 보장하기 위해 의도적으로 제한된 부분이라고 한다.
3. Next.js의 폰트 최적화 기능 이용하기
Next.js에서 로컬 폰트에 대해 최적화 기능을 제공한다.
import localFont from "next/font/local";
export const dunggeunmo = localFont({
src: "../app/fonts/dunggeunmo-subset.woff",
});
폰트 프리로딩을 위해서 next/font/local의 localFont를 사용하여 NextFont 인스턴스를 만들어주었다.
"use client";
import React from "react";
import { dunggeunmo } from "@/util/fonts";
export default function Modal() {
return (
<React.Fragment>
<div className={`${dunggeunmo.className} modal`}>
{...}
</div>
</React.Fragment>
);
}
그리고 폰트 프리로딩이 필요한 컴포넌트에서 NextFont 인스턴스의 className을 적용해주었다.
폰트가 필요한 컴포넌트가 렌더링되기 전에 폰트가 프리로딩되어 FOUT 현상이 나타나지 않게 되었다.
reference
지연 시간 없이 웹폰트 서빙하기 - Feat. Safari & Edge functions
'Next.js' 카테고리의 다른 글
Next.js 기초 개념 정리 (0) | 2023.04.14 |
---|