[Next.js 15] 동적으로 메타 태그 생성하기 Dynamic Metadata

Dynamic Metadata가 필요한 경우

검색 페이지에서 검색어에 따라 title 항목이 {검색어} 검색 결과 | 웹소설 캘린더라는 동적인 메타데이터를 생성하고 싶었다. 이처럼 페이지의 콘텐츠나 URL 파라미터에 따라 메타데이터가 변경되어야 하는 경우에는 동적 메타데이터 생성이 필요하다.

 

Next.js의 Metadata 생성 방법

Next.js는 SEO를 개선하기 위해 애플리케이션 메타데이터(head 요소 내부의 metalink 태그)를 자동으로 생성해 주는 API를 제공한다.

 

Static Metadata (정적 메타데이터)

// layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: '...',
  description: '...',
}
 
export default function Page() {}
  • 정적 메타데이터를 정의하기 위해서는 layout.tspage.js 파일에서 Metadata 객체를 export하면 된다.

 

Dynamic Metadata (동적 메타데이터)

// app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'
 
type Props = {
  params: Promise<{ id: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}
 
export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // read route params
  const { id } = await params
 
  // fetch data
  const product = await fetch(`https://.../${id}`).then((res) => res.json())
 
  // optionally access and extend (rather than replace) parent metadata
  const previousImages = (await parent).openGraph?.images || []
 
  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}
 
export default function Page({ params, searchParams }: Props) {}
  • 동적인 메타데이터가 필요한 경우에는 generateMetadata() 함수를 사용한다.
  • generateMetadata() 함수는 서버 컴포넌트에서만 지원된다.

 

메타데이터 제목에 템플릿 사용하기

// app/layout.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: {
    template: '%s | Acme',
    default: 'Acme', // a default is required when creating a template
  },
}
// app/about/page.tsx
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: 'About',
}
 
// Output: <title>About | Acme</title>
  • 메타데이터 title에 일관된 형식을 적용하기 위해 템플릿을 사용할 수 있다.
  • title.template을 사용해서 title에 접두사 또는 접미사를 추가한다.
  • 템플릿을 사용하면 기본적으로는 default 제목이 적용되고, 별도로 메타데이터 title 항목을 정의한 페이지에서만 template 구조의 제목이 생성된다.

 

실제 구현 사례: 검색 페이지

기본 템플릿 설정

// src/app/layout.tsx
import type { Metadata } from "next";

export const metadata: Metadata = {
  title: {
    template: "%s | 웹소설 캘린더",
    default: "웹소설 캘린더",
  },
}
  • 메인 페이지에서는 "웹소설 캘린더"를 보여주고, 다른 페이지에서는 "something | 웹소설 캘린더" 형식으로 제목을 생성한다.

 

동적 메타데이터 구현

import { Metadata } from "next";

type Props = {
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
};

// Dynamic Metadata 생성
export async function generateMetadata({
  searchParams,
}: Props): Promise<Metadata> {
  const { q } = await searchParams;
  const searchQuery = q || "";

  return {
    title: searchQuery ? `${searchQuery} 검색 결과` : "검색",
    description: "관심 있는 작가나 작품을 검색할 수 있어요.",
  };
}
  • 검색 전에는 "검색 | 웹소설 페이지"를, 검색 후에는 "검색어 검색 결과 | 웹소설 캘린더"로 제목을 생성한다.
  • 검색 페이지 URL이 /search?q=검색어 형태로 구성되어 있기 때문에 searchParams를 받아서 사용했다.

 

generateMetadata 함수의 파라미터와 주의사항

파라미터 종류

etc-image-0
etc-image-1

  • generageMetadata() 함수는 두 개의 파라미터를 받을 수 있다.
  • Path Variable를 사용한다면 params를 사용한다.
  • Query Parameter를 사용한다면 searchParams를 사용한다.

 

비동기 처리 주의사항

searchParams should be awaited before using its properties.
  • generageMetadata() 함수는 Dynamic API이기 때문에 paramssearchParams에 직접 접근할 수 없고, await를 사용해 비동기적으로 접근해야 한다.
  • await을 사용하지 않으면 위와 같은 에러 메시지가 표시된다.

 

서버 컴포넌트 적용

// src\app\search\page.tsx
export default function Page() {
  return <SearchPage />;
}
  • generateMetadata()는 서버 컴포넌트에서만 사용할 수 있는데, 기존의 검색 페이지는 이미 클라이언트 컴포넌트였다.
  • 그래서 search/page.tsx 파일 내부의 코드를 따로 SearchPage라는 컴포넌트로 분리해서  search/page.tsx를 서버 컴포넌트로 만들었다.

 

구현 결과

동적 메타데이터 구현 결과.gif

이제 검색 페이지의 URL이 /search?q=검색어일 때, 페이지 제목이 검색어 검색 결과 | 웹소설 캘린더로 동적으로 변경된다.

 

 

 

References