Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata

2026.07.15·수정 2026.09.12·약 19분·작성: 해비·블로그 소개

학습 목표·선수 지식: metadata 병합과 파일 우선순위를 실제 출력으로 진단합니다. 선수 지식: layout·page.

Next.js Metadata API 핵심 요약

App Router에서는 변하지 않는 정보는 metadata 객체로, 경로 매개변수나 데이터 조회 결과에 따라 달라지는 정보는 generateMetadata로 선언합니다. 현재 App Router의 params는 Promise이므로 params: Promise<{ slug: string }>로 타입을 지정하고 값을 쓰기 전에 await params 해야 합니다.

검증 기준: 2026년 7월 19일 확인한 Next.js 16.2.10 공식 문서, App Router, TypeScript 예제입니다. 버전이 달라지면 프로젝트의 설치 버전과 해당 버전 문서를 먼저 확인하세요.

Metadata API로 무엇을 관리할까

Metadata API는 단순히 검색 제목만 넣는 기능이 아닙니다. 문서 제목과 설명, canonical, Open Graph, X 카드, robots 지시, 아이콘, 사이트 소유권 확인 값을 route segment 단위로 관리합니다. Next.js는 이 값을 해석해 필요한 <title>, <meta>, <link> 요소를 만듭니다.

항목 주로 두는 위치 확인할 점
metadataBase, title 템플릿 root layout.tsx 사이트 전체 기준 URL과 이름을 한곳에서 관리
페이지 title·description page.tsx 페이지 검색 의도와 실제 본문이 일치하는지 확인
canonical·Open Graph URL 각 page 또는 동적 함수 모든 페이지가 홈 canonical을 상속하지 않도록 페이지별 지정
robots·googleBot 필요한 segment 사이트맵과 실제 색인 지시가 충돌하지 않는지 확인
아이콘·인증 root layout 또는 파일 기반 metadata 실제 파일과 발급받은 인증값 사용
themeColor viewport export metadata의 구식 필드로 넣지 않음

Google은 meta keywords를 검색 색인이나 순위에 사용하지 않는다고 명시합니다. 따라서 키워드 배열을 반복해 채우는 것보다 고유한 title, 본문을 정확히 요약한 description, 일관된 canonical을 우선하세요. description도 검색 결과에 항상 그대로 표시된다는 보장은 없습니다.

Next.js App Router metadata 선언 위치 구조

root layout과 page의 역할을 분리한다

root layout에는 여러 페이지가 공유하는 기본값만 둡니다. 페이지마다 달라지는 canonical이나 article Open Graph를 root에 고정하면 하위 페이지가 잘못된 URL을 내보낼 수 있습니다. title 템플릿은 layout에 두고, 실제 페이지 title은 page에서 지정하는 구조가 읽기 쉽습니다.

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

export const metadata: Metadata = {
  metadataBase: new URL("https://example.com"),
  title: {
    default: "Acme 개발 문서",
    template: "%s | Acme 개발 문서",
  },
  description: "Acme 제품과 개발 문서를 제공합니다.",
  applicationName: "Acme",
  icons: {
    icon: "/favicon.ico",
    apple: "/apple-touch-icon.png",
  },
  verification: {
    google: "실제로-발급받은-확인값",
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      "max-image-preview": "large",
      "max-snippet": -1,
      "max-video-preview": -1,
    },
  },
};

export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "#ffffff" },
    { media: "(prefers-color-scheme: dark)", color: "#111827" },
  ],
};

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

같은 목적의 <meta> 요소를 <head>에 다시 수동 작성하지 마세요. Metadata API와 수동 태그가 중복되면 어느 파일이 최종값을 만드는지 추적하기 어려워집니다. 단, resource hint처럼 Metadata API가 직접 다루지 않는 항목은 별도 API가 필요할 수 있습니다.

변하지 않는 페이지는 정적 metadata를 사용한다

소개, 가격, 고정 가이드처럼 요청마다 값이 달라지지 않는 페이지는 함수가 필요 없습니다. 정적 객체는 의도가 분명하고 빌드 시점에 결과를 확인하기 쉽습니다.

// app/guides/metadata/page.tsx
import type { Metadata } from "next";

const title = "Metadata API 가이드";
const description = "정적 metadata와 generateMetadata 사용 기준을 설명합니다.";

export const metadata: Metadata = {
  title,
  description,
  alternates: {
    canonical: "/guides/metadata",
  },
  openGraph: {
    type: "article",
    url: "/guides/metadata",
    title,
    description,
    images: [
      {
        url: "/images/metadata-guide.png",
        width: 1200,
        height: 630,
        alt: "Metadata API 가이드",
      },
    ],
  },
  twitter: {
    card: "summary_large_image",
    title,
    description,
    images: ["/images/metadata-guide.png"],
  },
};

export default function MetadataGuidePage() {
  return <main>가이드 본문</main>;
}

metadataBase가 상위 layout에 있으면 위의 상대 URL은 절대 URL로 조합됩니다. 상위에 기준 URL이 없는데 절대 URL이 필요한 필드에 상대 경로를 쓰면 빌드 오류가 날 수 있습니다. title 템플릿은 자신이 선언된 layout의 하위 segment에 적용되며 같은 segment의 page title에는 적용되지 않는다는 점도 함께 확인하세요.

동적 페이지는 Promise params를 await한다

상품이나 게시글처럼 URL의 slug로 데이터를 찾는 페이지에서는 generateMetadata를 사용합니다. Next.js 16의 App Router 예제에서 params는 Promise입니다. 예전의 동기식 params 타입과 값을 즉시 읽는 코드는 제거해야 합니다.

// app/posts/[slug]/page.tsx
import type { Metadata } from "next";
import { notFound } from "next/navigation";

type Props = {
  params: Promise<{ slug: string }>;
};

async function getPostBySlug(slug: string) {
  const response = await fetch(
    `https://api.example.com/posts/${encodeURIComponent(slug)}`,
  );
  if (response.status === 404) return null;
  if (!response.ok) throw new Error("게시글을 불러오지 못했습니다.");
  return response.json() as Promise<{
    title: string;
    description: string;
    image: string;
  }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);

  if (!post) notFound();

  return {
    title: post.title,
    description: post.description,
    alternates: { canonical: `/posts/${slug}` },
    openGraph: {
      type: "article",
      url: `/posts/${slug}`,
      title: post.title,
      description: post.description,
      images: [{ url: post.image, alt: post.title }],
    },
  };
}

export default async function PostPage({ params }: Props) {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) notFound();

  return (
    <article>
      <h1>{post.title}</h1>
    </article>
  );
}

metadata 객체와 generateMetadata 함수는 같은 route segment에서 동시에 export할 수 없고 둘 다 Server Component에서만 지원됩니다. 상호작용이 필요하면 page는 Server Component로 두고 버튼이나 폼만 별도 Client Component로 분리하세요.

정적 metadata와 동적 generateMetadata 비교

중첩 객체는 깊게 합쳐지지 않는다

route를 따라 평가된 metadata 객체는 얕게 병합됩니다. 뒤 segment가 openGraph를 새로 선언하면 앞 segment의 openGraph 내부 값이 자동으로 하나씩 보존되는 것이 아니라 해당 중첩 객체가 교체됩니다. robots 같은 다른 중첩 필드도 같은 방식으로 판단해야 합니다.

app/posts/[slug]/page.tsx에서 앞선 generateMetadata 함수만 교체하는 부분 코드입니다. 앞의 getPostBySlug와 notFound import 및 기본 page 함수는 그대로 유지합니다. Metadata·ResolvingMetadata 타입 import를 합치고 generateMetadata를 중복 export하지 않습니다.

import type { Metadata, ResolvingMetadata } from "next";

type Props = { params: Promise<{ slug: string }> };

export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata,
): Promise<Metadata> {
  const { slug } = await params;
  const post = await getPostBySlug(slug);
  if (!post) notFound();

  const inheritedImages = (await parent).openGraph?.images ?? [];

  return {
    title: post.title,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [post.image, ...inheritedImages],
    },
  };
}

상위 값을 정말 이어받아야 할 때만 parent를 읽어 명시적으로 합치세요. 페이지별 정보가 상위 기본값을 완전히 대체해야 한다면 억지로 합치지 않는 편이 맞습니다.

icons·verification·Viewport를 정확히 나눈다

icons는 favicon, shortcut icon, Apple touch icon을 표현할 수 있고, verification은 Google 등에서 실제 발급받은 소유권 확인값을 담습니다. 예제 문자열을 운영 환경에 그대로 복사하지 말고 서비스별 발급값을 사용하세요. 아이콘은 favicon.ico, icon.png, apple-icon.png 같은 파일 기반 metadata로 관리할 수도 있으며 파일 기반 값이 객체나 함수보다 우선합니다.

themeColorcolorSchememetadata에 넣는 방식은 deprecated입니다. 정적 값은 별도의 viewport: Viewport export로 옮깁니다. 기본 viewport는 Next.js가 자동으로 설정하므로 확대를 막는 userScalable: false 같은 접근성 저하 설정을 관성적으로 추가하지 마세요.

데이터 중복 조회와 스트리밍을 구분한다

generateMetadata, generateStaticParams, layout, page, Server Component에서 동일한 fetch 요청을 사용하면 Next.js가 자동으로 memoize합니다. fetch가 아닌 데이터베이스 함수라면 React cache로 요청 단위 중복 실행을 줄일 수 있습니다. 캐시 정책과 데이터 갱신 정책은 별도로 설계해야 하며, memoize가 영구 데이터 캐시를 뜻하는 것은 아닙니다.

동적으로 렌더링되는 페이지의 metadata는 UI가 먼저 전송된 뒤 스트리밍될 수 있습니다. metadata를 <head>에서 기대하는 봇에는 스트리밍이 비활성화되며, 사전 렌더링 페이지는 빌드 시 해결되므로 스트리밍하지 않습니다. 크롤러별 결과를 추측하지 말고 배포 후 최종 HTML과 공식 URL 검사 도구로 확인하세요.

배포 전에 확인할 체크리스트

  1. 각 URL의 title과 description이 실제 H1·본문을 정확히 요약하는지 확인합니다.
  2. canonical이 자기 자신의 최종 200 URL을 가리키고 리디렉션 URL이나 홈으로 잘못 고정되지 않았는지 확인합니다.
  3. Open Graph의 URL·이미지가 절대 URL로 해석되고 이미지 요청이 200인지 확인합니다.
  4. robots, googleBot, HTTP X-Robots-Tag, 사이트맵의 색인 정책이 서로 충돌하지 않는지 확인합니다.
  5. 같은 segment에 정적 metadatagenerateMetadata가 함께 export되지 않았는지 확인합니다.
  6. 동적 route의 모든 코드가 Promise params를 await하고, 없는 데이터는 notFound()로 처리하는지 확인합니다.
  7. 브라우저의 Elements만 보지 말고 페이지 원본, 소셜 미리보기, Search Console URL 검사 결과를 함께 확인합니다.

Client Component만으로 구성한 React SPA도 검색될 수는 있지만, JavaScript로 metadata를 뒤늦게 주입하면 크롤러와 공유 봇마다 처리 시점이 달라질 수 있습니다. Google도 JavaScript로 meta 태그를 주입하거나 바꿀 때 주의하고 충분히 테스트하라고 안내합니다. App Router에서는 서버에서 해결되는 Metadata API를 우선 사용하고 검색 노출을 라이브러리 선택 하나의 결과로 단정하지 마세요.

다음 단계

동적 metadata의 slug가 어디서 오는지 먼저 이해하려면 Next.js 동적 라우트 완전 정리에서 [slug], Promise params, generateStaticParams 흐름을 이어서 확인하세요. 사이트 전체 검색 구조는 Next.js SEO 완전 가이드에서 사이트맵·canonical·렌더링을 함께 점검할 수 있습니다.

중첩 Open Graph를 결과로 계산해 봅니다

부모 자식 최종 기대
openGraph: title·description·images openGraph: title만 부모 description·images가 자동 보존되지 않음
title.template: %s | 문서 하위 segment title: 설치 설치 | 문서
title.template: %s | 문서 하위 title.absolute: 설치 설치

연습: 자식 openGraph에 description과 images를 명시해 공유 미리보기를 복원하세요. 동적 예제의 api.example.com과 이미지 URL은 자신의 제공 API·파일로 교체하는 부분 통합 예제입니다. API가 반환하는 JSON의 TypeScript 단언은 런타임 데이터 검증을 대신하지 않습니다.

공식 문서

공식 기준과 확인 범위

확인일: 2026-09-12. API 설명은 Next.js 16 App Router 기준이며 과거 릴리스 글은 본문의 태그 범위를 유지합니다. 부분 API 코드는 기존 프로젝트에 통합하는 예시입니다.

공식 자료 재확인: 2026-09-12. 이 글의 실행하지 않은 브라우저/배포 점검은 독자 확인 과제로 구분합니다.

이 글이 도움이 되었나요?

조회 중

Next.js 학습 순서

필수 13개 · 전체 27개

읽음 기록 관리

전체 과정 목차 (27개)
  1. 필수 길잡이 · Next.js App Router 학습 순서: 설치부터 배포까지
  2. 필수 학습 · Next.js package.json: scripts dependencies 이해
  3. 필수 학습 · Next.js App Router로 만드는 첫 프로젝트 완성 실습 가이드
  4. 필수 학습 · Next.js에서 .next 폴더는 어떤 역할을 할까?
  5. 필수 학습 · Next.js 동적 라우트 완전 정리: [slug], params, catch-all
  6. 선택 참고 · Next.js params should be awaited 해결: App Router 기준
  7. 선택 참고 · Next.js window is not defined 오류 해결: 브라우저 API를 안전하게 쓰기
  8. 선택 참고 · Next.js hydration failed 오류 해결: 원인과 해결 방법
  9. 필수 학습 · Axios 사용법: React Next.js에서 API 요청 구조 잡는 법
  10. 선택 참고 · Next.js Route Handler 405 오류 해결: GET/POST 파일 위치와 메서드 설정 확인
  11. 필수 학습 · Next.js Server Actions + React Hook Form 검증 기준
  12. 선택 참고 · Next.js useSearchParams Suspense 오류 해결: 빌드 실패 기준
  13. 선택 참고 · Next.js fetch 캐시 문제 해결: 데이터가 바뀌었는데 화면이 그대로일 때
  14. 선택 참고 · Next.js Dynamic server usage 오류 해결: cookies headers 기준
  15. 선택 참고 · Next.js 환경변수 적용 오류 해결: .env.local을 바꿨는데 값이 안 바뀔 때
  16. 필수 학습 · Next.js Metadata API 완전 정리: 정적 metadata와 generateMetadata 현재 글
  17. 선택 참고 · Next.js metadata가 적용되지 않을 때 확인할 7가지
  18. 선택 참고 · Next.js Image remotePatterns 오류 해결: 외부 이미지 도메인 허용하기
  19. 필수 학습 · Next.js redirects 설정: next.config.js에서 URL 이동 처리
  20. 필수 학습 · Next.js SEO 체크리스트: metadata·초기 HTML·OG 이미지 점검
  21. 선택 참고 · Next.js SEO 완전 가이드: App Router metadata부터 배포 확인까지
  22. 필수 학습 · Next.js 16 성능 최적화 체크리스트: 번들·이미지·캐시·배포
  23. 필수 학습 · Next.js 렌더링 성능 최적화: React 화면이 느릴 때 기준
  24. 필수 학습 · Next.js 16 Proxy 마이그레이션: Node.js Runtime·matcher 검증
  25. 선택 참고 · Next.js 보안 패치 기준: v16.2.5 영향 범위 점검
  26. 선택 참고 · Next.js SEO SSR 적용법: 검색 노출과 렌더링 구조 잡기
  27. 시점·기록 · Next.js 16.3.0-canary.106의 useCache deprecation 경고와 hybrid not-found 수정 이해하기

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기