들어가며
웹 성능을 최적화하는 과정에서 중요한 측면 중 하나는 효율적인 폰트 로딩입니다. 이 글에서는 비효율적인 폰트 로딩으로 인해 발생한 LCP(Largest Contentful Paint) 문제를 해결하는 과정을 안내합니다. 다양한 폰트 형식 간의 차이점, 서브셋 사용의 장점, 그리고 이러한 개선 사항이 Next.js 14 버전을 사용하여 어떻게 구현되었는지 살펴보겠습니다.
문제 현상
처음 성능 분석에서 웹사이트의 폰트 로딩에 큰 지연이 발생한다는 것을 확인했습니다. 아래의 성능 분석 스크린샷에서 볼 수 있듯이, LCP가 약 7초에 달해 전체 사용자 경험에 큰 영향을 미쳤습니다. 이러한 지연의 주원인은 비효율적인 폰트 파일 로딩이었습니다.
원인 분석 및 해결 과정
부끄럽지만 저는 폰트 확장자에 대해 제대로 알지 못했습니다. 이번 문제를 해결하기 위해, 폰트 파일의 확장자와 크기에 대해 먼저 조사했습니다. 웹 폰트는 다양한 형식으로 제공되며, 각 형식은 파일 크기, 호환성 및 성능 측면에서 다릅니다. 아래는 웹에서 사용되는 주요 폰트 확장자에 대한 설명입니다.
폰트 확장자 설명
Font 포맷 | 설명 | 장점 | 단점 |
TTF (TrueType Font) | - 1980년대 후반 애플과 마이크로소프트가 개발한 폰트 형식 - 주요 운영 체제에서 지원되며 벡터 기반의 스케일링과 힌팅 기능을 갖추고 있음 |
- 모든 주요 운영 체제와 브라우저에서 광범위하게 지원됨 - 벡터 기반으로 고해상도에서 선명한 텍스트 렌더링이 가능 |
- 파일 크기가 비교적 큼 - 웹 최적화 기능이 부족 |
WOFF (Web Open Font Format) | - 웹용으로 최적화된 폰트 형식 - TTF보다 파일 크기가 작고 압축된 형태로 제공됌 |
- 웹용으로 최적화되어 파일 크기가 작음 - 대부분의 최신 브라우저에서 지원됨 |
- IE와 같은 일부 구형 브라우저에서 지원되지 않음 |
WOFF2 | - WOFF의 개선된 버전 - 더욱 효율적인 압축을 통해 파일 크기 최소화 |
- WOFF보다 더 작은 파일 크기 - 더 나은 압축 효율성 |
- 최신 브라우저에서만 지원 |
아래의 스크린샷에서 실제로 볼 수 있듯이, 확장자에 따라 파일 크기에 큰 차이가 있습니다. WOFF2 변환기를 사용하여 파일 크기를 줄였습니다.
서브셋 폰트
여기에 그치지 않고 서브셋 폰트를 도입했습니다. 서브셋 폰트는 필요한 문자만 포함하여 폰트 파일의 크기를 줄이는 방식입니다. 이 방법을 통해 woff2 폰트 파일 크기를 더욱 줄일 수 있습니다. 서브셋 폰트를 만드는 데 사용한 도구는 여기에서 확인할 수 있습니다. 일본어 프로그램인데 자세한 변환 방식은 https://boramyy.github.io/dev/front-end/etc/font/ 해당 블로그에서 잘 설명해주고 있습니다. 이렇게 변환한 결과가 위의 스크린샷인데, 서브셋 폰트의 파일 크기가 가장 작은 것을 확인할 수 있습니다.
로컬 폰트 사용 이유
Next.js 공식 문서에서는 localFont 사용이 성능에 좋지 않다고 안내하고 있습니다. 그러나 기본 CSS @font-face를 사용하면 폰트 로딩 시 깜박임 문제가 발생할 수 있기에, 이를 방지하기 위해 localFont를 사용하여 커스텀 폰트를 적용했습니다. 다만 구글 폰트를 사용한다면 localFont는 사용할 필요가 없습니다.
display: swap 적용
localFont의 옵션 중에 성능과 직결되는 옵션이 있습니다. 바로 display:swap입니다. display: swap은 폰트 로딩 중에 텍스트가 기본 폰트로 먼저 렌더링되고, 폰트 파일이 로드되면 해당 폰트로 교체되도록 합니다. 자세한 작동 방식은 아래와 같습니다.
- 기본 폰트 표시:
웹페이지가 로드되면 브라우저는 기본 시스템 폰트로 텍스트를 먼저 렌더링함 - 폰트 로드 대기:
브라우저는 비동기적으로 웹폰트를 로드함 - 폰트 교체:
폰트 로드가 완료되면 기본 폰트를 웹폰트로 교체함
이 과정에서 사용자는 텍스트가 기본 폰트로 표시되므로, 페이지 로딩이 완료될 때까지 빈 화면을 보지 않게 됩니다. 폰트가 로드되면 텍스트가 자연스럽게 교체되며, 이때 발생하는 변화는 깜박임으로 인식되지 않을 정도로 짧습니다.
최종 코드
폰트 최적화를 위해 모든 폰트를 WOFF2 형식으로 통일하고, 서브셋 폰트를 생성하여 적용했습니다. Next.js에서 로컬 폰트를 적용한 최종 코드는 다음과 같습니다:
import type { Metadata } from 'next';
import localFont from 'next/font/local';
import './styles/globals.css';
import 'react-toastify/dist/ReactToastify.css';
import OverlayProvider from '@/components/modal/OverlayProvider';
import { NextAuthProvider } from '@/context/NextAuthProvider';
import { Flip, ToastContainer } from 'react-toastify';
import ReactQueryClientProvider from '@/hooks/ReactQueryClientProvider';
import RecoilWrapper from '@/context/RecoilWrapper';
import UseLoading from '@/hooks/handlers/useLoading';
const pretendardBoldFont = localFont({
src: '../../public/font/Pretendard-Bold.woff2',
variable: '--font-pretendard-bold',
display: 'swap',
});
const pretendardRegularFont = localFont({
src: '../../public/font/Pretendard-Regular.woff2',
variable: '--font-pretendard-regular',
display: 'swap',
});
const gmarketBoldFont = localFont({
src: '../../public/font/GmarketSansTTFBold.woff2',
variable: '--font-gmarket-bold',
display: 'swap',
});
const gmarketMediumFont = localFont({
src: '../../public/font/GmarketSansTTFMedium.woff2',
variable: '--font-gmarket-medium',
display: 'swap',
});
export const metadata: Metadata = {
title: {
template: '%s | Lingda',
default: '링크 다이어리 | Lingda',
},
applicationName: '링다',
description: '간편하게 링크와 메모, 북마크를 관리해보세요!',
authors: [{ name: 'Lingda Team' }],
icons: { icon: './icon.ico' },
appleWebApp: { capable: false },
openGraph: {
type: 'website',
url: 'https://lingda.app',
title: '링크 다이어리 | Lingda',
description: '간편하게 링크와 메모, 북마크를 관리해보세요!',
images: [{ url: 'https://lingda.app/lingda-og-image.png' }],
locale: 'ko_KR',
},
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html
lang="ko"
className={`${pretendardBoldFont.variable} ${pretendardRegularFont.variable} ${gmarketBoldFont.variable} ${gmarketMediumFont.variable}`}
>
<body>
<ReactQueryClientProvider>
<OverlayProvider>
<NextAuthProvider>
<RecoilWrapper>
<UseLoading>
<main>{children}</main>
</UseLoading>
</RecoilWrapper>
</NextAuthProvider>
</OverlayProvider>
<ToastContainer
position="bottom-center"
autoClose={1500}
hideProgressBar={true}
closeOnClick
theme="dark"
transition={Flip}
/>
</ReactQueryClientProvider>
</body>
</html>
);
}
결과
위의 방법을 적용한 후, lighthouse 성능 분석에서 LCP가 7초에서 1초로 줄어들었으며, 사용자 경험을 크게 향상시킬 수 있었습니다.
이번 사례를 통해 폰트 최적화가 웹 성능에 얼마나 큰 영향을 미치는지 확인할 수 있었습니다. 폰트 형식을 효율적으로 선택하고, 서브셋 폰트를 사용하는 방법을 통해 웹 페이지 로딩 속도를 크게 개선할 수 있습니다. 혹시 더 개선할 부분을 알고 계시거나, 잘못된 부분이 있다면 언제든 알려주시길 바랍니다!
- 참고 문헌
https://d2.naver.com/helloworld/4969726
https://kyportfolio.tistory.com/194
https://boramyy.github.io/dev/front-end/etc/font/
https://velog.io/@dusunax/next.js-google-font-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0feat.-tailwind