[Next.js] 폰트 최적화

Simon·2024년 9월 8일
0
post-thumbnail

팀 프로젝트를 진행하고 있는데 Next.js에서 폰트 최적화가 있다고 말씀하셔서 Next.js에서의 폰트최적화가 무엇인지 공식 문서를 보면서 내용을 정리하고 실제로 어떻게 적용했는지 간단하게 기록하려고한다.

폰트 최적화

next/font는 개인정보 보호 및 성능 향상을 위해 font(사용자 정의 font 포함)를 자동으로 최적화하고 외부 네트워크 요청을 제거한다.

next/font에는 모든 글꼴 파일에 대한 자동 자체 호스팅이 내장되어 있다. 이건 사용된 기본 CSS size-adjust 속성 덕분에 레이아웃 변경 없이 웹 폰트를 최적으로 로드(load)할수 있다는 것이다.

여기서 레이아웃 변경 없다는 건 무슨 의미일까?

웹 페이지가 로드될 때, 폰트가 아직 로드되지 않았으면 시스템 기본 폰트나 대체 폰트를 사용하게 되는데, 이 대체 폰트와 실제 웹 폰트는 크기나 모양이 다를 수 있다. 이로 인해 레이아웃이 변경되거나 텍스트가 튀는 현상(layout shift)이 발생할 수 있다.

이를 해결하기 위해 CSS의 size-adjust 속성은 다음과 같은 역할을 한다.

대체 폰트와 실제 폰트 크기 차이를 최소화: size-adjust는 대체 폰트의 크기를 실제 폰트와 유사하게 맞추어 로드된 후 레이아웃이 크게 변하지 않도록 한다.

레이아웃 안정성 제공: 폰트가 늦게 로드되어도 레이아웃이 변경되지 않고, 페이지의 시각적 안정성이 유지된다.

추가로 CSS 및 글꼴 파일은 빌드 시 다운로드되며 나머지 정적 자산과 함께 자체 호스팅된다. 브라우저는 요청을 Google로 전송하지 않는다.

Google Fonts

모든 Google 글꼴을 자동으로 자체 호스팅한다. 폰트는 배포에 포함되며 배포자와 동일한 도메인에서 제공된다.

Google Fonts API를 통해 폰트를 외부 서버에서 불러오는 대신, 폰트를 직접 다운로드하여 웹사이트 서버에 저장하고 그 폰트를 제공하는 방식이다. 이를 통해 브라우저가 폰트를 Google 서버에서 요청하는 대신, 같은 도메인에서 폰트를 로드하게 된다.

이제 사용법을 알아보자. 정말 간단하다. 파일의 상단에서 next/font/google로 부터 사용할 폰트를 import하고 layout.tsx 파일의 body의 className 속성으로 값을 주면 된다. Next.js에서는 최고의 성능과 유연성을 위해 가변폰트를 사용하는 것을 권장하고 있다.

참고로 내가 사용하는 폰트는 구글 가변 폰트 목록에 없다. 하지만 구글 가변 폰트 목록에 없다고 해서 적용할 수 없는게 아니니 걱정하지 않아도 된다.
app/layout.tsx

import { Inter } from 'next/font/google'

// If loading a variable font, you don't need to specify the font weight
const inter = Inter({ subsets: ['latin'] })


export default function RootLayout({
 children,
}: Readonly<{
 children: React.ReactNode;
}>) {
 return (
   <html lang="en">
     <body className={inter.className}>{children}</body>
   </html>
 );
}

가변 글꼴을 사용할 수 없는 경우 weight를 지정해야 한다.

app/layout.tsx

import { Roboto } from 'next/font/google'

const roboto = Roboto({
 weight: '400',
 subsets: ['latin'],
})


export default function RootLayout({
 children,
}: Readonly<{
 children: React.ReactNode;
}>) {
 return (
   <html lang="en">
     <body className={roboto.className}>{children}</body>
   </html>
 );
}

배열을 사용하여 여러 가중치 및/또는 스타일을 지정할 수도 있다.

const roboto = Roboto({
  weight: ['400', '700'],
  style: ['normal', 'italic'],
  subsets: ['latin'],
  display: 'swap',
})

여기서 display: 'swap' 부분은 뭐냐면 폰트가 로드되기 전에 시스템 기본 폰트가 대체하여 보여지는 설정이다. 폰트 로딩 지연으로 인한 "FOUT" 현상(텍스트가 안 보이는 문제)을 방지할 수 있다고 한다.

알아두면 좋은 점: 여러 단어로 구성된 폰트 이름에는 (_)를 사용한다. Roboto Mono => Roboto_Mono

싱글 페이지 사용

하나의 페이지에서 폰트를 적용하려면 아래처럼 특정한 페이지에 추가하면 된다.

app/page.tsx

import { Inter } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'] })
 
export default function Home() {
  return (
    <div className={inter.className}>
      <p>Hello World</p>
    </div>
  )
}

subset 지정하기

Google 폰트는 자동으로 subset이다. 이것은 폰트 파일의 크기가 줄어들고 성능이 향상된다. 어떤 하위 집합을 preload 할지 정의할 필요가 있다.

폰트 서브셋(subset): 서브셋은 폰트 파일에서 특정 언어 또는 문자 집합만을 포함한 축소된 폰트 파일이다. 예를 들어, 한글, 영어, 라틴 문자 등을 따로 서브셋으로 만들어 각각 필요한 폰트 파일만 제공할 수 있다. 이렇게 하면 전체 폰트 파일 크기를 줄여서 웹사이트 성능을 향상시킬 수 있다.

preload: Preload는 웹 페이지 로드 과정에서 브라우저가 폰트를 미리 로드하도록 지시하는 기능이다. 중요한 폰트를 미리 불러와 빠르게 렌더링하도록 할 수 있다. 그러나 서브셋을 명확하게 지정하지 않으면 preload를 사용할 때 경고가 발생할 수 있다.

함수 호출에 추가하여 수행

const inter = Inter({ subsets: ['latin'] })

Local Fonts 적용

이번에는 next/font/local을 import하고 src 속성에 파일 경로를 지정해주면된다. 이번에는 실제 파일에 어떻게 적용했는지 적어보겠다.

기존 app/layout.tsx 파일

import './globals.css';
import localFont from 'next/font/local';

const pretendard = localFont({
  src: '../static/font/Pretendard-Light.otf',
  display: 'swap',
  weight: '100 200 300 400 500 600 700 800 900',
  variable: '--font-pretendard',
});

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={pretendard.className}>{children}</body>
    </html>
  );
}

기존 app/layout.tsx 수정 파일

import './globals.css';
import localFont from 'next/font/local';

const pretendard = localFont({
  src: '../fonts/PretendardVariable.woff2',
  display: 'swap',
});

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en">
      <body className={pretendard.className}>{children}</body>
    </html>
  );
}

내가 설정한 파일은 아니지만 이 코드에 문제점은 가변 폰트가 아닌 일일이 weight를 지정, otf 형식의 파일 사용, 사용할 필요 없는 variable 속성 사용 이 정도로 보인다. 일단 파일 형식에 대해 말하겠다.

OTF(OpenType Font)는 인쇄물, 디자인 작업, 데스크톱 응용 프로그램에서 주로 사용되는 폰트 형식이다. 고해상도 인쇄나 디자인 소프트웨어에서 사용되며, Adobe와 같은 전문적인 그래픽 툴에서 자주 사용된다. 단점은 웹 환경에서 사용하기에는 파일 크기가 크고 최적화되지 않은 경우가 많다고 한다.

WOFF2(Web Open Font Format 2)는 웹에서 사용하기 위해 최적화된 폰트 형식이다. 압축된 파일 형식으로, 웹 성능을 향상시키기 위해 만들어졌다. 즉, WOFF2 폰트는 파일 크기가 매우 작아 웹사이트 로딩 속도가 빠르다. PretendardVariable.woff2는 가변 폰트로, 다양한 굵기나 스타일을 하나의 파일로 제공할 수 있어 성능 최적화에 유리하다.

위와 같은 내용으로 파일을 변경하였다. 폰트 파일에 대해서는 다운받은 깃헙 사이트를 보면 좋을 것 같다. 가변 폰트이기 때문에 기존처럼 weight를 지정할 필요도 없어졌다.

마지막으로 위에서 사용할 필요 없는 variable 속성 사용이라고 했는데 왜그러나면 사이트 전체에서 같은 폰트를 사용할 것이여서 전체적으로 적용만 하면 되기 때문이다.

Tailwind CSS 속성으로 사용

여기서 이제 variable 속성에 대한 사용에 대한 예시가 있다. next/font는 CSS 변수를 통해 Tailwind CSS와 함께 사용할 수 있다.

import { Inter } from 'next/font/google'

const inter = Inter({
 subsets: ['latin'],
 variable: '--font-inter',
})

export default function RootLayout({
 children,
}: Readonly<{
 children: React.ReactNode;
}>) {
 return (
   <html lang="en">
     <body className={`${inter.variable} font-sans`}>{children}</body>
   </html>
 );
}

css 변수 이름을 정의하고 변수 옵션으로 폰트를 사용할 수 있다.

tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    './pages/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
    './app/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['var(--font-inter)'],
        mono: ['var(--font-roboto-mono)'],
      },
    },
  },
  plugins: [],
}

이제 font-sans 및 font-mono 유틸리티 클래스를 사용하여 요소에 글꼴을 적용할 수 있다.

사전 로딩 (Preloading)

폰트를 미리 불러오는 방식이 웹 사이트의 경로(route)에 따라 다르게 작동한다. 폰트는 웹사이트의 모든 경로에서 전역적으로 사용되지 않으며, 특정 경로에서만 사전 로드된다.

  1. 단일 페이지에 폰트가 사용된 경우: 해당 폰트는 그 특정 페이지 경로에서만 미리 로드된다.
  • 예를 들어, about.html이라는 페이지에서만 폰트가 사용된다면, 폰트는 이 페이지 경로(/about)에서만 사전 로드된다.
  1. 앱 수준에서 폰트가 사용된 경우: 폰트가 커스텀 App 파일에서 사용되었다면, 해당 폰트는 웹사이트 내의 모든 경로에서 사전 로드된다.
  • 여기서 "커스텀 App"이란 하위의 여러 페이지에서 공통적으로 사용되는 폰트를 정의한 파일을 의미한다. 즉, 이 파일에 정의된 폰트는 사이트의 모든 페이지에서 사전 로드된다.

폰트 재사용

localFont 또는 Google font 기능을 호출할 때마다 해당 font는 애플리케이션에서 하나의 인스턴스로 호스팅된다. 따라서 여러 파일에 동일한 font 함수를 로드하면 동일한 font의 여러 인스턴스가 호스팅됩니다. 이 상황에서는 다음과 같이 하는 게 좋다.

  • 하나의 공유 파일에서 font loader function 호출

  • 상수로 export

  • 이 글꼴을 사용하려는 각 파일에서 상수를 import

이전에는 폰트를 사용하면 이런 부분들은 생각해 본적도 없고 link를 복사해서 사용하거나 파일을 다운해서 이용했는데 이렇게 다양한 사용 방법과 최적화에 대한 많은 부분이 있다는 걸 새롭게 알게된 것 같다. 공식 문서에 모든 내용은 다룬 건 아니지만 내가 font 최적화를 적용한 부분에 있어서는 충분한 것 같다. 더 자세한 내용은 공식 문서를 찾아보면 좋을 것 같다.

profile
포기란 없습니다.

0개의 댓글

관련 채용 정보