현인

Next.js 튜토리얼 - 3장 폰트 및 이미지 최적화 본문

기술 학습/NextJS

Next.js 튜토리얼 - 3장 폰트 및 이미지 최적화

현인(Hyeon In) 2024. 11. 8. 18:51

이전 챕터에서는 Next.js 어플리케이션의 스타일링 방법에 대해 알아봤다. 이번 장에서는 커스텀 폰트와 hero image를 추가하는 방법에 대해 다뤄볼 예정이다.

hero image란?
웹사이트나 애플리케이션에서 가장 상단에 큰 배너 형식으로 배치되는 이미지. 보통 첫 화면에서 사용자가 가장 먼저 보게 되는 이미지로, 브랜드의 메시지나 핵심 내용을 시각적으로 전달하는 역할을 한다. - https://vwo.com/glossary/hero-image/

3장에서 다룰 내용

  1. next/font를 활용한 커스텀 폰트 추가 방법
  2. next/image를 활용한 이미지 추가 방법
  3. Next.js에서 폰트와 이미지를 최적화하는 방법

폰트는 왜 최적화 해야 할까?

폰트는 웹사이트 디자인에서 중요한 역할을 한다. 하지만 커스텀 폰트를 활용할 경우 폰트 파일을 로딩하는 과정에서 성능에 영향을 미칠 수 있다.

누적 레이아웃 이동(CLS)은 Google에서 웹사이트의 성능과 사용자 경험을 평가하는데 사용하는 지표다. 폰트와 레이아웃 이동이 어떤 연관이 있을까.

폰트의 경우 브라우저가 fallback이나 시스템 폰트로 먼저 렌더링을 하게 되고, 커스텀 폰트가 로드된 이후 커스텀 폰트로 교체한다. 커스텀 폰트의 텍스트 크기, 간격이 시스템 폰트와 다를 경우 레이아웃이 이동될 수 있다. 아래 그림을 보면 이해가 될 것이다.

Next.js는 next/font 모듈을 활용하여 자동으로 폰트를 최적화한다. 폰트 파일의 다운로드를 빌드 타임에 진행하여 다른 애셋 파일과 함께 제공한다. 이 말은 사용자가 웹사이트를 방문했을 때 폰트를 위한 추가적인 네트워크 요청이 필요하지 않아 성능에 영향을 주지 않는다는 것이다.

기본 폰트 추가

어떻게 동작하는지 알아보기 위해 어플리케이션에 커스텀 Google 폰트를 추가해 보자.

/app/ui 폴더에 fonts.ts 파일을 만든다. 이 파일을 사용하여 어플리케이션 전체에서 사용될 폰트를 보관한다.

next/font/google 모듈에서 Inter 폰트를 가져온다. 이 폰트가 기본 폰트가 된다. 그 다음 로딩될 하위 집합을 지정한다. 아래 경우에선 ‘latin’을 사용했다.

import { Inter } from 'next/font/google';
 
export const inter = Inter({ subsets: ['latin'] });

마지막으로 /app/layout.tsx 파일 내 <body> 요소에 폰트를 추가한다.

import '@/app/ui/global.css';
import { inter } from '@/app/ui/fonts';
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body className={`${inter.className} antialiased`}>{children}</body>
    </html>
  );
}

Inter 폰트를 <body> 요소에 추가하면 폰트가 어플리케이션 전체에 적용된다. 여기서 Tailwind도 추가하고 있다. Tailwind ‘antialiased’ 클래스는 폰트를 매끄럽게 한다. 굳이 사용하지 않아도 되지만 부드러운 느낌을 더해준다. (필자는 차이를 잘 모르겠다)

브라우저를 열어서 폰트가 잘 적용되었는지 확인해보자. 개발자 도구를 열고 요소를 선택하여 폰트 영역에 Inter, Inter Fallback이 나오면 잘 적용된 것이다.

보조 폰트 설정 연습

해당 튜토리얼을 진행하다보면 중간중간 퀴즈가 있다. 이번에는 직접 코드를 짜보라고 한다.

위에서 진행한 방식대로 Lusitana 폰트를 보조 폰트로 사용하여 /app/page.tsx 파일 내 <p> 태그에 폰트를 적용해보라고 했다. 단, Lusitana 폰트는 font weight 값도 정해주어야 한다고 했다.

우선 필자는 다음과 같이 fonts.ts 파일을 수정했다.

import { Inter, Lusitana } from "next/font/google";

const inter = Inter({ subsets: ["latin"] });
const lusitana = Lusitana({ weight: ["400", "700"], subsets: ["latin"] });

export { inter, lusitana };

그리로 /app/page.tsx 파일을 다음과 같이 수정했다.

import AcmeLogo from "@/app/ui/acme-logo";
import { ArrowRightIcon } from "@heroicons/react/24/outline";
import Link from "next/link";
import styles from "@/app/ui/home.module.css";
// lusitana 폰트 불러오기
import { lusitana } from "@/app/ui/fonts";

export default function Page() {
  return (
    <main className="flex min-h-screen flex-col p-6">
      <div className="flex h-20 shrink-0 items-end rounded-lg bg-blue-500 p-4 md:h-52">
        {/* <AcmeLogo /> */}
      </div>
      <div className="mt-4 flex grow flex-col gap-4 md:flex-row">
        <div className="flex flex-col justify-center gap-6 rounded-lg bg-gray-50 px-6 py-10 md:w-2/5 md:px-20">
          {/* CSS modules로 정의한 클래스로 변경 */}
          <div className={styles.shape} />
          {/* lusitana 폰트 적용 */}
          <p
            className={`${lusitana.className} text-xl text-gray-800 md:text-3xl md:leading-normal`}
          >
       // ...
  );
}

이렇게 수정했을 경우 아래와 같은 화면을 보았다.

폰트가 잘 바뀐 모습이다. 튜토리얼에서 해답 코드도 제공하니 필요한 경우 튜토리얼 문서에서 확인하면 된다. 필자의 코드와 크게 다르지 않았다.

마지막으로, <AcmeLogo /> 컴포넌트역시 Lusitana 폰트를 사용한다. 에러가 발생할 수 있어서 주석처리되어 제공되었지만 이제 주석을 제거해도 에러가 나지 않을 것이다. 최종적인 화면은 다음과 같다.

이제 hero image를 추가해보자.

왜 이미지 최적화가 필요한가?

Next.js에서는 최상위 폴더 아래에 /public에서 이미지와 같은 정적 애셋을 제공할 수 있다. 따라서 /public 내부 파일들은 어플리케이션에서 참조할 수 있다.

일반 HTML을 사용한다면 다음과 같이 이미지를 추가할 수 있다.

<img
  src="/hero.png"
  alt="Screenshots of the dashboard project showing desktop version"
/>

하지만 이런 방식으로 이미지를 추가한다면 다음과 같은 사항들을 직접 확인해야 한다.

  • 다양한 화면 크기에서 이미지가 반응하는지 확인
  • 다양한 장치에 맞게 이미지 크기 지정
  • 이미지가 로드될 때 레이아웃이 전환되는 것을 방지
  • 뷰포트 밖에 있는 이미지를 Lazy Loading

이미지 최적화는 웹 개발에서 큰 주제로, 그 자체로 전문 분야로 간주될 수 있다. 이러한 최적화를 수동으로 구현하는 대신 next/image 컴포넌트를 사용하여 이미지를 자동으로 최적화 할 수 있다.

<Image> 컴포넌트

<Image> 컴포넌트는 HTML <img> 태그를 확장한 것이며, 다음과 같은 자동 이미지 최적화 기능들을 제공한다.

  • 이미지 로딩 시 레이아웃이 자동으로 전환되는 것을 방지
  • 작은 뷰포트를 갖춘 기기에 큰 이미지가 전송되는 것을 방지하기 위해 이미지 크기 조절
  • Lazy Loading이 기본적으로 적용되어 뷰포트에 이미지가 들어오면 로드됨
  • WebP나 AVIF 같은 최신 형식 이미지가 지원되는 브라우저일 경우 최신 형식으로 이미지 제공

데스크톱 hero image 추가

이제 <Image> 컴포넌트를 사용해보자. /public 폴더를 보면, hero-desktop.png, hero-mobile.png 두 개의 이미지가 들어있다. 이 두 이미지는 사용자 장치에 따라 다르게 보여주기 위함이다.

/app/page.tsx 파일에서 next/image 컴포넌트를 가져온다. 그 다음 주석 아래에 이미지를 추가한다.

import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image';
 
export default function Page() {
  return (
    // ...
    <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
      {/* Add Hero Images Here */}
      <Image
        src="/hero-desktop.png"
        width={1000}
        height={760}
        className="hidden md:block"
        alt="Screenshots of the dashboard project showing desktop version"
      />
    </div>
    //...
  );
}

코드를 조금 살펴보면, width, height 어트리뷰트를 통해 이미지 픽셀을 설정할 수 있는데, 이 때 layout shift를 피하기 위해 소스 이미지와 동일한 종횡비로 설정해야 한다.

“hidden md:block” 클래스를 보면, hidden을 통해 기본적으로 안보이게 설정을 하고, md 사이즈의 디바이스일 경우 이미지를 보이게 설정한다는 뜻이다. 실제로 실행을 시켜보면, 디바이스의 가로 폭이 768px 미만일 경우 이미지가 사라지게 된다.

아래와 같이 이미지가 제대로 나오는지 확인해보고, 개발자 도구를 통해 폭을 조절하며 이미지가 사라지는지도 확인해보자.

모바일 사이즈로 줄이니 이미지가 사라진 모습

모바일 hero image 추가 연습

위에서 배운대로 <Image> 컴포넌트를 사용하여 직접 모바일 이미지를 추가해보자. 이미지는 hero-mobile.png 를 사용하면 되고, 가로 폭은 560, 세로폭은 620이다. 모바일 이미지이기 때문에 모바일 사이즈에서만 보여야 하며, 데스크톱 사이즈에선 원래대로 데스크톱 이미지가 나와야 한다.

필자는 아래와 같이 작성했다.

import AcmeLogo from '@/app/ui/acme-logo';
import { ArrowRightIcon } from '@heroicons/react/24/outline';
import Link from 'next/link';
import { lusitana } from '@/app/ui/fonts';
import Image from 'next/image';
 
export default function Page() {
  return (
    // ...
    <div className="flex items-center justify-center p-6 md:w-3/5 md:px-28 md:py-12">
      {/* Add Hero Images Here */}
      <Image
        src="/hero-desktop.png"
        width={1000}
        height={760}
        className="hidden md:block"
        alt="Screenshots of the dashboard project showing desktop version"
      />
      <Image
        src="/hero-mobile.png"
        width={560}
        height={620}
        className="block md:hidden"
        alt="Screenshot of the dashboard project showing mobile version"
      />
    </div>
    //...
  );
}

이제 모바일 사이즈로 화면을 줄이면 모바일 사이즈 hero image를 볼 수 있다.

추천 관련 문서

폰트와 이미지를 다루면서 최적화와 관련된 이야기를 나누었다. 이 주제에 대해 더 자세히 알아보고 싶으면 아래 문서들을 통해 배워보자. 현대 웹 개발에서 렌더링 최적화는 아주아주 중요한 이슈라고 생각한다.

마치며

이미지 최적화는 여러 번 경험해 보았지만, 폰트로 인해 CLS가 나빠진다는 것은 이번 학습을 통해 처음 알게 되었다. 당연한 부분을 놓치고 있었던 것 같아서 의미 있는 시간이 되었다.

학습 자료

https://nextjs.org/learn/dashboard-app/optimizing-fonts-images

 

Learn Next.js: Optimizing Fonts and Images | Next.js

Optimize fonts and images with the Next.js built-in components.

nextjs.org

반응형