Next.js의 App Router는 강력한 메타데이터 API를 제공합니다. 정적인 페이지는 export const metadata를 사용하면 되지만, 블로그 포스트, 상품 상세 페이지, 연예인 프로필 등 동적으로 변하는 페이지의 SEO를 극대화하려면 generateMetadata가 필수입니다.
정적 vs 동적 메타데이터
-
정적 메타데이터 (
metadata): 고정된 페이지(홈, 소개, 문의 등)에 사용합니다. -
동적 메타데이터 (
generateMetadata): URL 파라미터나 외부 API 데이터(DB)에 따라 메타데이터가 변경되어야 하는 페이지에 사용합니다.
generateMetadata 기본 구현 패턴
동적 라우트([id] 또는 [slug])에서 파라미터를 받아와 메타데이터를 생성하는 가장 표준적인 구조입니다.
// app/posts/[slug]/page.tsx
import { Metadata, ResolvingMetadata } from 'next';
interface Props {
params: Promise<{ slug: string }>;
searchParams: Promise<{ [key: string]: string | string[] | undefined }>;
}
// 1. generateMetadata 함수 구현
export async function generateMetadata(
{ params, searchParams }: Props,
parent: ResolvingMetadata
): Promise<Metadata> {
// Promise로 넘어오는 params 처리
const { slug } = await params;
// 외부 API 또는 DB에서 데이터 조회
const response = await fetch(`https://api.example.com/posts/${slug}`);
const post = await response.json();
// (선택사항) 부모 레이아웃의 메타데이터를 가져와 조합할 수도 있습니다.
const previousImages = (await parent).openGraph?.images || [];
return {
title: `${post.title} | 서비스명`,
description: post.summary,
openGraph: {
title: post.title,
description: post.summary,
url: `https://example.com/posts/${slug}`,
siteName: '서비스명',
images: [post.thumbnailUrl, ...previousImages],
type: 'article',
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.summary,
images: [post.thumbnailUrl],
},
};
}
// 2. 실제 페이지 컴포넌트
export default async function Page({ params }: Props) {
const { slug } = await params;
return <main>... {slug} 포스트 내용 ...</main>;
}
Next.js 15+ 변경 사항 적용: Next.js 15부터는
params와searchParams가 Promise 형태로 제공되므로, 내부에서await를 사용해 언랩(Unwrap)해야 타입을 정확히 맞출 수 있습니다.
실무 최적화 팁과 주의사항
① fetch 중복 호출 걱정은 NO (데이터 캐싱)
generateMetadata 내부에서 fetch로 데이터를 호출하고, 아래 실제 Page 컴포넌트에서 똑같은 URL을 fetch하면 "성능에 문제가 생기지 않을까?" 걱정할 수 있습니다.
-
Next.js가 자동으로 중복을 제거(Deduplication)합니다.
-
동일한 요청은 한 번만 수행되므로 성능 저하 걱정 없이 편하게 두 번 호출해도 됩니다. (단, Axios나 자체 DB 직접 접근 라이브러리는 별도의 캐싱 처리가 필요할 수 있으므로
React.cache사용을 권장합니다.)
② 오픈그래프(OG) 이미지 절대 경로 사용
openGraph.images에 들어가는 URL은 반드시 https://...로 시작하는 절대 경로여야 합니다. 상대 경로(/images/og.png)를 쓰면 크롤러가 이미지를 제대로 인식하지 못합니다.
③ 다국어 및 Canonical URL 설정
중복 콘텐츠 문제를 방지하기 위해 alternates 속성으로 표준(Canonical) URL을 지정하는 것이 SEO 점수에 매우 좋습니다.
return {
title: post.title,
alternates: {
canonical: `https://example.com/posts/${slug}`,
languages: {
'en-US': `https://example.com/en/posts/${slug}`,
'ko-KR': `https://example.com/ko/posts/${slug}`,
},
},
};
구조화된 데이터(JSON-LD)와 함께 사용하기
SEO의 끝판왕은 구글 검색 결과에 리치 스니펫(별점, 이미지, 작성자 등)을 노출하는 JSON-LD 구조화 데이터입니다. generateMetadata와 연계하여 페이지 컴포넌트에 직접 주입하면 SEO 효과를 극대화할 수 있습니다.
// app/posts/[slug]/page.tsx
export default async function Page({ params }: Props) {
const { slug } = await params;
const post = await getPost(slug); // 캐싱된 데이터 호출
// JSON-LD 스키마 정의
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'NewsArticle',
'headline': post.title,
'image': [post.thumbnailUrl],
'datePublished': post.createdAt,
'author': [{
'@type': 'Person',
'name': post.authorName,
}],
};
return (
<main>
{/* 구글 크롤러 인식용 JSON-LD 주입 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
<h1>{post.title}</h1>
{/* 본문 내용 */}
</main>
);
}
최종 체크리스트
-
[ ]
params를 다룰 때await params처리를 해주었는가? -
[ ] OG 이미지 주소에 절대 경로(
https://...)를 사용했는가? -
[ ] 검색 로봇이 중복 페이지로 인식하지 않도록
canonical설정을 더했는가? -
[ ] 글로벌 서비스를 타겟팅한다면
alternates.languages설정을 고려했는가?