정적 메타데이터 설정
layout과 page의 metadata 객체로 제목·설명·canonical·아이콘을 선언해 검색 엔진에 페이지 정보를 전달합니다.
검색 엔진 최적화(SEO)는 웹사이트가 검색 결과에 더 잘 노출되도록 만드는 필수 과정이며, 그 핵심 요소 가운데 하나가 메타데이터(metadata) 설정입니다. 메타데이터는 페이지 내용을 검색 엔진과 소셜 플랫폼에 설명해, 페이지가 어떻게 색인되고 노출될지에 큰 영향을 줍니다. App Router는 라우트마다 정적 메타데이터(static metadata)를 선언하는 API를 제공합니다.
13장에서는 정적 메타데이터, 동적 메타데이터, Open Graph 태그, robots.txt와 sitemap.xml을 차례로 다루며 검색 엔진과 소셜 공유를 위한 설정을 정리합니다. 이 글에서는 정적 메타데이터의 역할, App Router에서 설정하는 방법, SEO에 영향을 주는 주요 메타데이터 속성을 다룹니다.
메타데이터란 무엇이며 왜 중요한가요?
메타데이터는 "데이터에 대한 데이터"를 뜻하며, 웹 페이지에서는 그 페이지의 콘텐츠를 설명하는 정보입니다. HTML <head> 태그 안의 <meta> 태그, <title> 태그 등으로 정의됩니다.
메타데이터는 세 가지 면에서 중요합니다. 검색 엔진 크롤러는 메타데이터를 읽어 페이지의 주제와 관련성을 파악하고 검색 결과에 반영합니다. 다만 메타데이터는 페이지를 설명하는 신호일 뿐, 설정만으로 특정 순위나 색인이 보장되지는 않습니다. Open Graph(OG)와 Twitter Card 메타데이터는 페이지가 Facebook, X(Twitter), LinkedIn 같은 소셜 미디어에 공유될 때 제목, 설명, 이미지의 미리보기가 어떻게 보일지 정의하며, 링크 클릭률을 높이는 데 중요한 역할을 합니다. 브라우저 탭의 제목이나 북마크 이름도 메타데이터로 정해지므로, 분명한 제목과 설명은 사용자에게 페이지의 내용을 미리 알려 줍니다.
App Router에서 정적 메타데이터 설정하기
App Router에서 정적 메타데이터를 설정하는 가장 일반적이고 권장되는 방법은 layout.tsx나 page.tsx 파일에서 metadata 객체를 export하는 것입니다. Next.js는 라우트의 metadata 선언을 해석해 필요한 태그를 만듭니다. 정적 객체라는 말은 값이 고정되어 있다는 뜻이지, 라우트 전체가 빌드할 때만 렌더링된다는 뜻은 아닙니다. 이 export는 서버 컴포넌트에서 씁니다.
기본 메타데이터 설정
애플리케이션 전체에 적용할 기본 메타데이터는 루트 layout.tsx 파일에서 설정합니다. 아래 코드에서 볼 부분은 export const metadata와 export const viewport 두 선언입니다. 컴포넌트 함수는 메타데이터를 다루지 않으며, Next.js가 내보낸 객체를 읽어 <head>에 들어갈 태그를 만듭니다.
import type { Metadata, Viewport } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
const inter = Inter({ subsets: ['latin'] });
// 1. Metadata 객체 export
export const metadata: Metadata = {
title: 'Next.js 튜토리얼 - 성능 최적화와 SEO', // 웹사이트의 기본 제목
description: 'Next.js App Router를 사용하여 성능 최적화 및 SEO 설정을 학습하는 튜토리얼입니다.', // 웹사이트의 기본 설명
keywords: ['Next.js', 'React', '성능 최적화', 'SEO', '웹 개발'], // 관련 키워드
authors: [{ name: 'Your Name', url: 'https://personal-lab.dev' }], // 개발자 정보
creator: 'Your Name', // 제작자 정보
publisher: 'Your Company', // 발행자 정보
// Open Graph (소셜 미디어 공유 최적화)
openGraph: {
title: 'Next.js 튜토리얼 - 성능 최적화와 SEO',
description: 'Next.js App Router를 사용하여 성능 최적화 및 SEO 설정을 학습하는 튜토리얼입니다.',
url: 'https://personal-lab.dev', // 소셜 그래프에서 페이지를 식별하는 URL
siteName: 'Next.js 학습 사이트', // 웹사이트 이름
images: [
{
url: 'https://personal-lab.dev/og-image.png', // 소셜 미디어 공유 시 표시될 이미지
width: 1200,
height: 630,
alt: 'Next.js 튜토리얼 이미지',
},
],
locale: 'ko_KR', // 언어 및 지역 (예: 한국어)
type: 'website', // 페이지 타입 (website, article 등)
},
// Twitter Card (트위터 공유 최적화)
twitter: {
card: 'summary_large_image', // 카드 타입 (summary, summary_large_image, app, player)
title: 'Next.js 튜토리얼 - 성능 최적화와 SEO',
description: 'Next.js App Router를 사용하여 성능 최적화 및 SEO 설정을 학습하는 튜토리얼입니다.',
creator: '@yourtwitterhandle', // 트위터 계정
images: ['https://personal-lab.dev/twitter-image.png'], // 트위터 공유 시 표시될 이미지
},
// 페이지의 색인·링크 처리 지침 (robots.txt의 크롤링 규칙과 별개)
robots: {
index: true, // 이 페이지를 인덱싱할지 여부
follow: true, // 이 페이지의 링크를 따라갈지 여부
nocache: true, // 크롤러별 지원이 다른 지시어이며 HTTP 캐시 설정이 아님
googleBot: { // GoogleBot 전용 설정
index: true,
follow: false,
noimageindex: true, // 이미지 인덱싱 방지
'max-video-preview': -1,
'max-snippet': -1,
},
},
};
// viewport와 themeColor는 Metadata가 아니라 Viewport API로 선언합니다.
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
themeColor: '#FFFFFF',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ko">
<body className={inter.className}>{children}</body>
</html>
);
}루트 레이아웃의 metadata는 모든 하위 페이지의 기본값이 됩니다. 예를 들어 openGraph.images는 og:image, og:image:width 같은 태그로, twitter.card는 twitter:card 태그로 바뀌어 각 페이지의 <head>에 들어갑니다. 그래서 레이아웃의 JSX에는 <title>이나 <meta>를 직접 쓰지 않고, 이 객체로만 선언합니다.
페이지별 메타데이터 설정
특정 페이지에만 적용할 메타데이터는 그 페이지의 page.tsx에서 metadata 객체를 export해 설정합니다. 페이지에서 선언한 필드는 상위의 같은 필드보다 우선하고, 선언하지 않은 필드는 상위의 값을 이어받습니다.
import type { Metadata } from 'next';
// 1. Dashboard 페이지의 metadata 객체 export
export const metadata: Metadata = {
title: '대시보드 - 내 계정 요약', // 이 페이지의 고유 제목 (루트 레이아웃의 제목을 덮어씀)
description: '사용자 계정의 대시보드입니다. 최신 활동, 알림 및 설정에 접근하세요.', // 이 페이지의 고유 설명
// Open Graph 등 다른 속성도 여기에 추가하거나 덮어쓸 수 있습니다.
openGraph: {
title: '대시보드 - Next.js 학습',
description: '나만의 Next.js 대시보드 페이지입니다.',
images: ['https://personal-lab.dev/dashboard-og.png'],
},
// robots: { index: false, follow: false }, // 특정 페이지를 검색에서 제외하고 싶을 때
};
export default function DashboardPage() {
return (
<div>
<h1>대시보드</h1>
<p>환영합니다! 이곳은 대시보드 페이지입니다.</p>
</div>
);
}메타데이터 병합 규칙
Next.js는 루트 레이아웃에서 페이지로 내려오며 메타데이터를 해석하지만, 모든 필드를 깊게 병합하지는 않습니다. title, description 같은 단순 필드는 가까운 하위 세그먼트의 값이 상위 값을 덮어씁니다. openGraph, robots 같은 중첩 필드도 하위 객체가 상위 객체를 통째로(얕게) 바꿉니다. 그래서 하위 openGraph에 description만 쓰면 상위의 openGraph.images가 저절로 남지 않습니다. 상위의 배열이나 중첩 값을 함께 쓰려면 공통 변수를 import하거나, generateMetadata에서 parent를 읽어 명시적으로 합칩니다.
NEXT · 13.1
같은 페이지에서도 필드마다 선택되는 선언이 다릅니다
본문의 루트 layout과 Dashboard page를 함께 적용하고 별도 메타데이터 파일이 없을 때의 선언 해석입니다. 하위 openGraph 객체의 교체와 선언하지 않은 필드의 상속을 구분합니다.
| 필드 | 선택되는 선언 | 본문 코드에서 확인할 점 |
|---|---|---|
| title·description | page의 값 | 같은 최상위 필드에 가까운 세그먼트의 값이 적용됩니다. |
| openGraph | page의 객체 | title·description·images를 다시 선언합니다. 루트의 siteName·locale·url은 자동으로 이어지지 않습니다. |
| 루트 layout의 객체 | page에서 선언하지 않아 루트의 기본 카드 내용이 남습니다. | |
| robots | 루트 layout의 객체 | page의 robots 예시는 주석이므로 실행되는 설정이 아닙니다. Googlebot용 follow는 false입니다. |
- title·description
- 선택되는 선언: page의 값본문 코드에서 확인할 점: 같은 최상위 필드에 가까운 세그먼트의 값이 적용됩니다.
- openGraph
- 선택되는 선언: page의 객체본문 코드에서 확인할 점: title·description·images를 다시 선언합니다. 루트의 siteName·locale·url은 자동으로 이어지지 않습니다.
- 선택되는 선언: 루트 layout의 객체본문 코드에서 확인할 점: page에서 선언하지 않아 루트의 기본 카드 내용이 남습니다.
- robots
- 선택되는 선언: 루트 layout의 객체본문 코드에서 확인할 점: page의 robots 예시는 주석이므로 실행되는 설정이 아닙니다. Googlebot용 follow는 false입니다.
정적 metadata는 값이 고정된 선언입니다. 라우트가 빌드 때만 렌더된다는 뜻은 아닙니다. 이 비교는 외부 검색·공유 결과를 관측한 표가 아니며, openGraph.url은 검색용 canonical을 대신하지 않습니다.
위 표는 루트 레이아웃과 Dashboard 페이지를 함께 적용하고 별도의 메타데이터 파일이 없을 때, 필드마다 어느 선언이 선택되는지 정리합니다. title과 description은 같은 최상위 필드이므로 가까운 세그먼트인 페이지의 값이 적용됩니다. openGraph는 페이지가 객체를 선언했으므로 루트의 openGraph가 통째로 바뀌며, 페이지 객체에 없는 루트의 siteName, locale, url도 이어지지 않습니다. twitter는 페이지에서 선언하지 않았으므로 루트 레이아웃의 기본 카드 내용이 남습니다. robots는 페이지의 예시가 주석이라 실행되는 설정이 아니므로 루트의 값을 쓰며, Googlebot용 follow는 false입니다.
표의 아래 설명처럼 정적 metadata는 값이 고정된 선언일 뿐 라우트가 빌드할 때만 렌더링된다는 뜻이 아니며, 이 비교는 외부 검색이나 공유 결과를 직접 확인한 표가 아닙니다. 또한 openGraph.url은 검색용 canonical을 대신하지 않습니다.
주요 메타데이터 속성 상세 설명
Metadata 타입은 SEO와 소셜 미디어에 관련된 여러 속성을 제공합니다.
title은 <title> 태그에 해당하는 값으로 브라우저 탭에 표시되며, 검색 결과의 제목을 정하는 데 쓰일 수 있지만 검색 엔진이 다른 문구를 고를 수도 있습니다. 페이지 내용을 간결하고 분명하게 요약하며, 페이지 제목 | 웹사이트 이름이나 웹사이트 이름 - 페이지 제목 같은 형식을 권장합니다. description은 <meta name="description" content="..." />에 해당하며 검색 결과의 요약(snippet)으로 쓰일 수 있습니다. 페이지 내용을 한두 문장으로 요약하고 관련 키워드를 담는 것이 좋습니다.
keywords는 <meta name="keywords" content="..." />에 해당합니다. Google 검색은 이 태그를 색인이나 순위에 쓰지 않으며, 위 코드는 Metadata API가 제공하는 필드를 선언하는 예시일 뿐입니다. authors, creator, publisher는 콘텐츠의 저자, 제작자, 발행자 정보를 제공합니다.
openGraph는 Facebook, LinkedIn처럼 Open Graph 프로토콜을 쓰는 소셜 미디어에 페이지가 공유될 때 보일 정보를 정의하며, title, description, url, siteName, images, locale, type 등의 속성이 있습니다. images는 배열로 여러 이미지를 지정할 수 있습니다. twitter는 Twitter Card를 정의해 X(Twitter)에 공유될 때 보일 정보를 제어하며, summary나 summary_large_image 같은 card 타입과 title, description, creator, images 등의 속성이 있습니다.
robots는 <meta name="robots" content="..." />에 해당하며, 검색 엔진 크롤러에게 페이지를 어떻게 처리할지 지시합니다. index와 follow가 주요 속성이고, index: false는 그 페이지를 검색 결과에서 빼라는 뜻입니다. 크롤러가 이 지침을 읽으려면 페이지에 접근할 수 있어야 하므로, robots.txt로 막는 것이 색인 제외나 인증을 대신하지 않습니다. nocache도 브라우저, CDN, Next.js의 Data Cache를 제어하지 않습니다.
화면 배율과 themeColor는 Viewport API로 선언합니다. viewport 객체나 generateViewport 함수를 쓰며, 이 값들은 Metadata 객체에 넣지 않습니다.
icons는 파비콘(favicon)과 그 밖의 웹 아이콘을 설정합니다. 다음은 기존 metadata 객체에 넣을 icons 필드의 예시이므로, 같은 파일에 두 번째 metadata export를 추가하지 않습니다.
import type { Metadata } from 'next';
export const metadata: Metadata = {
icons: {
icon: '/logo.png', // 기본 파비콘 대체 경로
shortcut: '/logo.png',
apple: '/logo.png', // iOS 홈 화면 아이콘
other: {
rel: 'apple-touch-icon-precomposed',
url: '/logo.png',
},
},
};각 키는 서로 다른 <link> 태그가 됩니다. icon은 rel="icon", shortcut은 rel="shortcut icon", apple은 rel="apple-touch-icon"이 되고, other에는 rel 값을 직접 적습니다. /logo.png처럼 /로 시작하는 경로는 public 폴더의 파일을 가리키므로, 파일이 그 위치에 없으면 태그는 만들어져도 아이콘은 표시되지 않습니다.
정적 메타데이터 설정 시 고려사항
웹사이트 전체에서 메타데이터의 형식과 내용을 일관되게 유지합니다. 페이지마다 고유하고 관련성 높은 제목과 설명을 주어 중복 콘텐츠로 인한 SEO 문제를 피하고, keywords나 description에 키워드를 지나치게 늘어놓지 말고 자연스러운 문장으로 씁니다. 메타데이터는 웹 페이지의 실제 내용을 정확히 반영해야 하며, 모바일 기기에서 잘 보이도록 viewport를 설정합니다.
최종 og:image와 Twitter 이미지 URL은 절대 URL이어야 합니다. Metadata API에서는 루트에 metadataBase를 지정하면 /og-image.png 같은 상대 경로를 쓸 수 있고, Next.js가 이를 절대 URL로 해석합니다. 어떤 방식을 쓰든 실제 공개 주소에서 이미지가 열리는지 확인합니다.
대표 URL도 구분합니다. openGraph.url은 소셜 그래프에서 페이지를 식별하는 주소이며, 검색용 <link rel="canonical">이 필요하면 alternates.canonical을 따로 선언합니다. 위 예제에는 이 필드가 없습니다. 페이지를 배포한 뒤에는 Google Search Console, Facebook의 공유 디버거 같은 도구로 메타데이터가 올바르게 인식되는지 확인합니다.
App Router의 정적 메타데이터 기능을 쓰면 페이지별 제목, 설명, 공유 정보를 코드 가까이에 선언할 수 있으며, 검색 노출과 소셜 공유 미리보기는 이 메타데이터의 일관성에 영향을 받습니다.
정리
- 정적 메타데이터는 서버 컴포넌트인
layout.tsx나page.tsx에서metadata객체를export해 선언합니다. - 하위 세그먼트의 필드가 상위의 같은 필드를 덮어쓰며,
openGraph나robots같은 중첩 객체도 깊게 병합되지 않고 통째로 바뀝니다. - 선언하지 않은 필드는 상위의 값을 이어받으므로,
openGraph를 바꿔도twitter는 그대로 남습니다. viewport와themeColor는Metadata가 아니라 Viewport API로 선언하고, 검색용 대표 URL은alternates.canonical로 따로 지정합니다.- 공유 이미지는 최종적으로 절대 URL이어야 하며,
metadataBase를 지정하면 상대 경로를 쓸 수 있습니다.
연습 문제
1. 최상위 필드와 중첩 객체 비교하기
위 두 파일을 함께 쓰고 별도의 메타데이터 파일은 없다고 가정합니다. Dashboard의 브라우저 제목, OG 이미지, Twitter 이미지, Googlebot의 링크 지침은 각각 어느 파일의 선언에서 선택될까요? 페이지의 주석 처리된 robots 예시를 실제 설정으로 바꾸면 무엇을 다시 선언해야 할까요?
해설 보기
제목과 openGraph.images는 Dashboard 페이지의 값입니다. twitter와 robots는 페이지에서 선언하지 않았으므로 루트의 값을 이어받으며, Googlebot의 follow는 false입니다. OG 이미지를 바꿨다고 Twitter 이미지까지 Dashboard 이미지가 되지는 않습니다.
페이지에 robots 객체를 실제로 선언하면 그 객체가 상위의 robots를 통째로 바꿉니다. 상위의 Googlebot 세부 지침까지 유지하려면 필요한 값을 명시적으로 함께 구성해야 합니다. 상속을 판단할 때는 주석과 실제로 실행되는 선언을 먼저 구별하세요.
2. 상대 경로로 쓴 공유 이미지
metadataBase 없이 페이지의 openGraph.images를 ['/dashboard-og.png']처럼 상대 경로로 바꿨습니다. 어떤 문제가 생길 수 있고, 어떻게 고치면 좋을까요?
해설 보기
og:image는 최종적으로 절대 URL이어야 소셜 플랫폼의 크롤러가 이미지를 가져올 수 있습니다. 상대 경로만 있으면 어떤 도메인 기준인지 알 수 없으므로, 루트 레이아웃의 metadata에 metadataBase: new URL('https://personal-lab.dev')처럼 기준 주소를 지정합니다. 그러면 Next.js가 /dashboard-og.png를 그 주소 기준의 절대 URL로 해석합니다. 그 뒤에도 실제 공개 주소에서 이미지가 열리는지 확인합니다.
3. 클라이언트 컴포넌트에서 metadata 내보내기
"use client"를 붙인 페이지 파일에서 export const metadata = { title: '설정' }을 선언했습니다. 이 선언은 동작할까요?
해설 보기
동작하지 않습니다. metadata 객체와 generateMetadata 함수는 서버 컴포넌트에서만 지원되므로, 클라이언트 컴포넌트 파일에서 내보내면 Next.js가 오류를 냅니다. 상호작용이 필요한 부분만 별도의 클라이언트 컴포넌트로 분리하고, 페이지 파일은 서버 컴포넌트로 둔 채 metadata를 내보냅니다.
정적 메타데이터의 필드와 병합 규칙은 Next.js 공식 문서의 generateMetadata와 metadata 레퍼런스에서 확인할 수 있습니다.
다음 글 동적 메타데이터 생성에서는 generateMetadata로 라우트 파라미터와 데이터에 따라 메타데이터를 만드는 방법을 알아봅니다.