8. 왜 metadata를 선언만했는데 seo가 챙겨지는건가
2025년 12월 13일
Next.js가 메타데이터를 인식하는 방법
1. 파일 기반 메타데이터 인식
Next.js App Router는 파일 시스템 기반으로 메타데이터를 자동 인식합니다.
인식 규칙:
export const metadata또는export async function generateMetadata를 찾음- 파일이
page.tsx또는layout.tsx인지 확인 - 해당 파일의 메타데이터를 수집하여 HTML에 주입
파일 구조:
app/
layout.tsx ← 루트 레이아웃 메타데이터
page.tsx ← 홈페이지 메타데이터
dashboard/
page.tsx ← 대시보드 메타데이터
detail/
[category_middle]/
page.tsx ← 동적 라우트 메타데이터
2. 빌드 타임 vs 런타임 처리
정적 페이지 (Static Pages):
// app/dashboard/page.tsx export const metadata: Metadata = { title: "대시보드", description: "...", };
- 빌드 타임에 메타데이터 수집
- HTML에 미리 포함
- 빠른 로딩
동적 페이지 (Dynamic Pages):
// app/detail/[category_middle]/page.tsx export async function generateMetadata({ params }): Promise<Metadata> { // 런타임에 데이터 가져오기 const data = await fetchData(params); return { title: `${data.name} 분석`, }; }
generateStaticParams가 있으면 빌드 타임에 각 경로에 대해 실행- 없으면 런타임에 실행
- 각 경로별로 다른 메타데이터 생성 가능
3. 메타데이터 병합 (Metadata Merging)
Next.js는 계층적으로 메타데이터를 병합합니다.
병합 순서:
- 루트 레이아웃 (
app/layout.tsx) - 중첩 레이아웃 (
app/dashboard/layout.tsx- 있다면) - 페이지 (
app/dashboard/page.tsx)
예시:
// app/layout.tsx export const metadata: Metadata = { title: { default: "Crime Insight 2019", template: "%s | Crime Insight 2019", // 템플릿 정의 }, description: "기본 설명", }; // app/dashboard/page.tsx export const metadata: Metadata = { title: "대시보드", // "대시보드 | Crime Insight 2019"로 변환됨 description: "대시보드 설명", // 기본 설명을 덮어씀 };
최종 결과:
title: "대시보드 | Crime Insight 2019" (템플릿 적용)description: "대시보드 설명" (페이지에서 덮어씀)
왜 선언만 하면 되는가?
1. Next.js의 자동 처리 메커니즘
Next.js는 내부적으로 다음과 같이 처리합니다:
1단계: 메타데이터 수집
// Next.js 내부 (의사 코드) function collectMetadata(route) { const layoutMetadata = getLayoutMetadata(route); const pageMetadata = getPageMetadata(route); return mergeMetadata(layoutMetadata, pageMetadata); }
2단계: HTML 변환
// Next.js 내부 (의사 코드) function generateHeadTags(metadata: Metadata) { return ` <title>${metadata.title}</title> <meta name="description" content="${metadata.description}"/> <meta property="og:title" content="${metadata.openGraph?.title}"/> <!-- 기타 메타 태그들 --> `; }
3단계: HTML 주입
- 서버 컴포넌트 렌더링 시
<head>에 자동 주입 - 클라이언트 사이드 네비게이션 시에도 업데이트
2. React Server Components와의 통합
Next.js App Router는 React Server Components를 사용합니다:
// 서버 컴포넌트에서 메타데이터 선언 export const metadata: Metadata = { ... }; // Next.js가 서버에서 렌더링할 때 // 메타데이터를 수집하여 HTML에 포함
장점:
- 서버에서 한 번만 처리
- 클라이언트 번들 크기 감소
- SEO에 유리 (서버 렌더링)
실제 구현 내용
1. 루트 레이아웃 메타데이터
파일: app/layout.tsx
import type { Metadata } from "next"; export const metadata: Metadata = { title: { default: "Crime Insight 2019", template: "%s | Crime Insight 2019", // 모든 페이지 제목에 자동 추가 }, description: "범죄 발생의 시간적, 요일별 패턴을 분석하는 대시보드...", keywords: ["범죄 통계", "범죄 분석", "데이터 시각화", "대시보드", "범죄 패턴"], authors: [{ name: "Crime Insight Team" }], openGraph: { type: "website", locale: "ko_KR", url: "https://crime-insight.vercel.app", siteName: "Crime Insight 2019", title: "Crime Insight 2019", description: "범죄 발생의 시간적, 요일별 패턴을 분석하는 대시보드", }, twitter: { card: "summary_large_image", title: "Crime Insight 2019", description: "범죄 발생의 시간적, 요일별 패턴을 분석하는 대시보드", }, };
설명:
title.template: 모든 하위 페이지 제목에 " | Crime Insight 2019" 자동 추가openGraph: 소셜 미디어 공유 최적화twitter: 트위터 카드 최적화'- url은 배포후 변경해야함
2. 정적 페이지 메타데이터
홈페이지 (app/page.tsx)
import type { Metadata } from "next"; export const metadata: Metadata = { title: "홈", description: "범죄 발생의 시간적, 요일별 패턴을 분석하여 데이터 기반 인사이트를 제공합니다...", openGraph: { title: "Crime Insight 2019 - 범죄 발생 패턴 분석", description: "언제, 어디서 범죄가 가장 많이 발생할까?", }, }; export default function Home() { // 컴포넌트 내용 }
결과:
- 브라우저 탭: "홈 | Crime Insight 2019"
- 검색 결과: "Crime Insight 2019 - 범죄 발생 패턴 분석"
About 페이지 (app/about/page.tsx)
import type { Metadata } from "next"; export const metadata: Metadata = { title: "프로젝트 소개", description: "Crime Insight 2019 프로젝트 소개. 공공데이터포털 경찰청 범죄 발생 시간대 및 요일 데이터를 기반으로 한 범죄 통계 분석 대시보드입니다...", openGraph: { title: "프로젝트 소개 | Crime Insight 2019", description: "공공데이터포털 경찰청 범죄 발생 데이터를 기반으로 한 범죄 통계 분석 대시보드 프로젝트 소개", }, };
대시보드 페이지 (app/dashboard/page.tsx)
import type { Metadata } from "next"; export const metadata: Metadata = { title: "대시보드", description: "범죄 발생 통계 대시보드. 요일별, 시간대별 범죄 발생 현황을 차트로 확인하고, 범죄 유형별 상세 통계를 검색할 수 있습니다.", openGraph: { title: "범죄 통계 대시보드 | Crime Insight 2019", description: "요일별, 시간대별 범죄 발생 현황과 범죄 대분류 비중을 시각화한 대시보드", }, };
3. 동적 라우트 메타데이터 (핵심)
파일: app/detail/[category_middle]/page.tsx
동적 라우트는 generateMetadata 함수를 사용합니다:
import type { Metadata } from "next"; interface DetailPageProps { params: Promise<{ category_middle: string; }>; } // 동적 메타데이터 생성 함수 export async function generateMetadata({ params }: DetailPageProps): Promise<Metadata> { // 1. params에서 동적 경로 값 추출 const { category_middle } = await params; const categoryName = decodeURIComponent(category_middle); // 2. 데이터 가져오기 (서버에서 실행) const records = parseCrimeData(); const crimeData = findCrimeByCategoryMiddle(records, categoryName); // 3. 데이터가 없으면 404 메타데이터 반환 if (!crimeData) { return { title: "페이지를 찾을 수 없습니다", description: "요청하신 범죄 유형을 찾을 수 없습니다.", }; } // 4. 범죄별 맞춤 메타데이터 생성 return { title: `${categoryName} 범죄 분석`, description: `${categoryName} 범죄의 시간대별, 요일별 발생 패턴을 분석한 상세 통계. 전체 범죄 평균과 비교하여 ${categoryName} 범죄의 특성을 확인할 수 있습니다. 총 ${crimeData.total.toLocaleString()}건의 데이터를 분석했습니다.`, keywords: [categoryName, "범죄 통계", "범죄 분석", crimeData.categoryMajor], openGraph: { title: `${categoryName} 범죄 분석 보고서 | Crime Insight 2019`, description: `${categoryName} 범죄의 시간대별, 요일별 발생 패턴과 전체 평균과의 비교 분석`, type: "article", }, twitter: { card: "summary_large_image", title: `${categoryName} 범죄 분석`, description: `${categoryName} 범죄 발생 패턴 분석 - 총 ${crimeData.total.toLocaleString()}건`, }, }; } export default async function DetailPage({ params }: DetailPageProps) { // 페이지 컴포넌트 내용 }
동작 과정:
-
빌드 타임 처리 (
generateStaticParams와 함께)export async function generateStaticParams() { const records = parseCrimeData(); const categoryMiddleList = extractCategoryMiddleList(records); return categoryMiddleList.map((categoryMiddle) => ({ category_middle: encodeURIComponent(categoryMiddle), })); }- 각 범죄 유형에 대해
generateMetadata실행 - 예: "살인기수", "강도", "사기" 등 38개 경로
- 각 경로별로 고유한 메타데이터 생성
- 각 범죄 유형에 대해
-
메타데이터 생성 예시
/detail/살인기수 → generateMetadata({ params: { category_middle: "살인기수" } }) → { title: "살인기수 범죄 분석", description: "살인기수 범죄의... 총 594건..." } /detail/사기 → generateMetadata({ params: { category_middle: "사기" } }) → { title: "사기 범죄 분석", description: "사기 범죄의... 총 608,944건..." } -
최종 HTML 생성
<!-- /detail/살인기수 페이지 --> <head> <title>살인기수 범죄 분석 | Crime Insight 2019</title> <meta name="description" content="살인기수 범죄의 시간대별, 요일별 발생 패턴을 분석한 상세 통계. 총 594건의 데이터를 분석했습니다."/> <meta property="og:title" content="살인기수 범죄 분석 보고서 | Crime Insight 2019"/> <!-- 기타 메타 태그들 --> </head>
Next.js의 내부 처리 흐름
1. 빌드 타임 (Static Generation)
1. Next.js가 모든 라우트 스캔
↓
2. page.tsx에서 metadata 또는 generateMetadata 찾기
↓
3. generateMetadata가 있으면 params로 실행
↓
4. 메타데이터를 수집하여 HTML에 포함
↓
5. 정적 HTML 파일 생성
2. 런타임 (Dynamic Rendering)
1. 사용자가 페이지 요청
↓
2. Next.js가 해당 라우트의 generateMetadata 실행
↓
3. 메타데이터 수집
↓
4. HTML <head>에 주입하여 응답
실제 적용 결과
검색 엔진 최적화 (SEO)
각 페이지마다 고유한 메타데이터:
- 홈: "홈 | Crime Insight 2019"
- 대시보드: "대시보드 | Crime Insight 2019"
- 살인기수 상세: "살인기수 범죄 분석 | Crime Insight 2019"
- 사기 상세: "사기 범죄 분석 | Crime Insight 2019"
소셜 미디어 공유
Open Graph 메타데이터로 공유 시:
- 페이스북/카카오톡: 제목, 설명, 이미지 표시
- 트위터: Twitter Card 형식으로 표시
사용자 경험
- 브라우저 탭에 명확한 페이지 제목
- 북마크 시 의미 있는 제목
- 브라우저 히스토리에서 페이지 구분 용이
기억해야 할 점
1. 선언만 하면 되는 이유
- Next.js가 파일 시스템 기반으로 자동 인식
export const metadata또는export async function generateMetadata만 선언- 내부적으로 HTML 변환 및 주입 처리
2. 정적 vs 동적
- 정적:
export const metadata(빌드 타임에 고정) - 동적:
export async function generateMetadata(경로별로 다르게 생성)
3. 메타데이터 병합
- 레이아웃 → 페이지 순서로 병합
title.template로 일관된 형식 유지
4. SEO 최적화
- 각 페이지마다 고유한 메타데이터
- Open Graph, Twitter Card 지원
- 검색 엔진 친화적
결론
Next.js App Router의 메타데이터 시스템은 선언만으로 동작합니다. 파일 기반 인식, 자동 HTML 변환, 계층적 병합을 통해 SEO와 소셜 공유를 쉽게 최적화할 수 있습니다. 특히 동적 라우트에서 generateMetadata를 사용하면 경로별 맞춤 메타데이터를 생성할 수 있어 효과적입니다.
원래는 사실 이번 글에서 배포를 하고 끝내려고 했는데, 사실 또 csr 말고 ssr쓰는 이유중 가장 큰게 seo 아니겠습니까. 이에, 왜 넥스트는 순수리액트와 다르게 seo를 챙길 수 잇는지, 또 어떤 방식으로 챙기게 되는건지 알아보고 싶었습니다.
이제 다음 글에서는 버셀에 배포를 완료하고자 합니다.