돈워리 프로젝트를 하면서 next.js에서 제공해주는 localFont를 활용하여 정적 폰트를 사용하였는데...
이 참에 선배 개발자분들의 노고와 문제를 해결한 방법을 살펴보고 최적화하는 방법을 정리해보려고 한다.
브라우저에서 폰트의 동작 원리를 살펴보자
브라우저는 글꼴을 렌더링하기 전에 렌더 트리를 구성해야한다. 그 말인 즉슨 DOM과 CSSOM이 만들어져 합쳐져야한다는 것이다. 그로 인해 폰트는 서버로부터 응답을 받을때까지 브라우저가 텍스트를 렌더링하지 못하는 경우가 있다. 그렇게 되면 유저가 페이지에 들어왔을때 무엇인가 보이고 난 후 레이아웃이 변경되면서 폰트가 적용되는 어릴 적 흔했던 웹페이지를 볼 수 있다.
위의 작업을 하나씩 정리해보면
브라우저에 내장되어있는 폰트만을 사용해야했고 특정 폰트를 사용하려면 해당 폰트를 서버에 호스팅하고 가지고 오는 방식으로 과거에는 구현했다고 한다. 혹은 이미지를 사용하여 텍스트를 렌더링하고 이미지에 원하는 폰트 스타일을 적용하는 방법도 사용됐다. 이런 문제를 해결하기 위해 ... 등장한 것이!
@font-face라는 기본 css를 활용하여 웹페이지의 텍스트에 온라인폰트를 적용할 수 있게 됐다. @font-face를 사용하여 개발자가 원하는 폰트를 사용할 수 있게 됐다. (그러면 이전에는 안됐다는건가?! ) 로컬 폰트만 사용해야했던 제약
@font-face statement는 font-family
, src
, font-weight
, font-style
등의 속성을 사용하여 폰트를 세밀하게 제어할 수 있습니다.
렌더 트리 이후 화면에 그려지는 단계에서 병목이 일어나 글꼴이 늦게 보여지는 문제를 어떻게 해결할 수 있을까였다.
첫번째, WebFonts를 미리 로드한다.
//html 파일의 header 내부에
<link rel="preload"></link>
를 추가하여 CSSOM이 생성되기 전 렌더링 경로 초기에 WebFont 요청을 트리거한다.
font-display를 알기전 사전 지식이 필요한데 그건 FOUT, FOIT
폰트의 변화로 발생하는 이 현상을 FOUT (Flash of Unstyled Text) 혹은 FOIT라고 한다.
예를 들어 사용하고 있는 브라우저의 기본 설정을 알아보자
TIP. 그런데 왜? 지난 프로젝트(알리고 올리고)에선 크롬인데도 불구하고 FOUT 현상이 일어났지?
3초만 기다리는 FOIT이기 때문이다. 폰트의 크기가 커 3초이상의 로드 시간이 걸려 약 1초에서 2초 사이에 텍스트가 보이고 그 위에 폰트가 덮어씌게되는것이다.
FOIT,FOUT를 알았으니 @font-face의 속성인 font-display를 알아보자
font-display
는 글꼴 다운로드 기간마다 글자를 어떻게 보여줄지 결정하는 CSS 속성인데 3가지 기간으로 구분된다.
속성에는 5가지가 있는데 간단히 살펴보면
미리 로드하는 방식과 font-display를 함께 사용해도 좋지만 추가 맞춤 설정이 필요하니 더 좋은 옵션을 소개해준다. Font Loading API에 대해 이번 블로그가 아닌 다른 블로그에서 알아보려고 한다.
압축률 좋은 포맷 사용하기
1. WOFF : 기존의 TTF와 OTF와 동일하게 동작하지만, 압축을 통해 더 작은 파일 크기를 가진다.
2. WOFF2 : WOFF보다 30~50% 정도 더 작은 파일 크기를 가지지만 호환하지 않을 확률이 높아 WOFF를 Fallback 폰트로 같이 사용한다.
<link rel=””>
속성으로 preload 옵션을 사용하면 해당 리소스를 다른 리소스보다 빨리 로딩한다. 주로 폰트 파일, 이미지 파일, 스크립트 파일, 비디오 파일 등 페이지에서 중요도가 높은 자원을 의도적으로 먼저 로딩할때 사용한다.
<link
rel="preload"
href="./nanumGothic.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
서브셋 폰트(Subset font)란 폰트 파일에서 불필요한 글자들을 제거하고 사용할 글자만 남겨둔 폰트이다. 이러한 불필요한 글자를 폰트에서 제거하고 사용할 글자만 남겨둔 폰트가 서브셋 폰트입니다. 보통 서브셋 폰트를 사용하면 용량이 pretendard 기준, 814KB → 273KB
까지 감소한다고 한다. (출처 : https://www.datoybi.com/Web-font-optimization/)
현재 진행중인 축의금 선정을 도와주는 서비스인 돈워리에서는 next.js를 사용하고 있다. 로컬 폰트를 활용하여 서버로 폰트 파일을 받지 않도록 구현하였고 next/font/local 모듈을 활용하고 있다. localFont의 타입을 살펴보면서 어떻게 구현되어있는지 살펴보려고 한다.
// fonts.ts
import localFont from 'next/font/local';
export const pretendard = localFont({
src: [
{
path: '../assets/fonts/Pretendard/Pretendard-Thin.woff2',
weight: '100',
},
{
path: '../assets/fonts/Pretendard/Pretendard-ExtraLight.woff2',
weight: '200',
},
{
path: '../assets/fonts/Pretendard/Pretendard-Light.woff2',
weight: '300',
},
{
path: '../assets/fonts/Pretendard/Pretendard-Regular.woff2',
weight: '400',
},
{
path: '../assets/fonts/Pretendard/Pretendard-Medium.woff2',
weight: '500',
},
{
path: '../assets/fonts/Pretendard/Pretendard-SemiBold.woff2',
weight: '600',
},
{
path: '../assets/fonts/Pretendard/Pretendard-Bold.woff2',
weight: '700',
},
{
path: '../assets/fonts/Pretendard/Pretendard-ExtraBold.woff2',
weight: '800',
},
{
path: '../assets/fonts/Pretendard/Pretendard-Black.woff2',
weight: '900',
},
],
variable: '--font-pretendard',
});
//layout.ts
import type { Metadata } from 'next';
import { ToastContainer } from '@/components/common/toast';
import { META } from '@/constants/metadata';
import Providers from '@/contexts/Providers';
import { pretendard } from './fonts';
import './globals.css';
// TODO: 메타 데이터 수정
export const metadata: Metadata = META;
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="ko">
<body className={`${pretendard.variable}`} suppressHydrationWarning={true}>
<Providers>
<ToastContainer />
<div className="layout font-pretendard ">{children}</div>
</Providers>
</body>
</html>
);
}
위의 코드와 같이 정적 폰트 파일을 가지고 있고 localFont 매소드의 인자로 객체 안에 넣어주고 있다. localFont의 타입을 살펴보면
type LocalFont<T extends CssVariable | undefined = undefined> = {
src: string | Array<{
path: string;
weight?: string;
style?: string;
}>;
display?: Display;
weight?: string;
style?: string;
adjustFontFallback?: 'Arial' | 'Times New Roman' | false;
fallback?: string[];
preload?: boolean;
variable?: T;
declarations?: Array<{
prop: string;
value: string;
}>;
};
export default function localFont<T extends CssVariable | undefined = undefined>(options: LocalFont<T>): T extends undefined ? NextFont : NextFontWithVariable;
localFont는 undefined를 기본으로 CssVariable과 undefined 타입을 상속 받은 제네릭 타입을 받고 있고 인자로는 LocalFont 타입에 T를 받고 있다.
//위에 보이는 타입들을 조금 더 깊이 살펴보자
export type CssVariable = `--${string}`;
export type Display = 'auto' | 'block' | 'swap' | 'fallback' | 'optional'; // 이부분은 font-display의 value값과 동일하다. 구현은 font-display로 되어있는 것 같다.
export type NextFont = {
className: string;
style: {
fontFamily: string;
fontWeight?: number;
fontStyle?: string;
};
};
export type NextFontWithVariable = NextFont & {
variable: string;
};
핵심적으로 localFont도 결국 @font-face를 보다 쉽게 사용할 수 있도록 구현해놓은 것이 아닐까 생각한다.
Next/font를 채택한 이유는 이러하다.
next/font
will automatically optimize your fonts (including custom fonts) and remove external network requests for improved privacy and performance.next/font
includes built-in automatic self-hosting for any font file. This means you can optimally load web fonts with zero layout shift, thanks to the underlying CSS size-adjust property used.점차 기본적인 개념들을 몰라도 개발하는데는 무리가 없어지고 있다고 느낀다. 필히 기술에 대한 역사와 배경은 알고 쓰자.
출처
1. 웹 프론트엔드 성능 최적화 교제
2. web.dev : https://web.dev/articles/optimize-webfont-loading?hl=ko
3. @font-face : https://developer.mozilla.org/ko/docs/Web/CSS/@font-face
4. Naver D2 : https://d2.naver.com/helloworld/4969726
5. Next.js 공식 문서 : https://nextjs.org/docs/pages/building-your-application/optimizing/fonts