logo

DowanKim

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는 계층적으로 메타데이터를 병합합니다.

병합 순서:

  1. 루트 레이아웃 (app/layout.tsx)
  2. 중첩 레이아웃 (app/dashboard/layout.tsx - 있다면)
  3. 페이지 (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) { // 페이지 컴포넌트 내용 }

동작 과정:

  1. 빌드 타임 처리 (generateStaticParams와 함께)

    export async function generateStaticParams() { const records = parseCrimeData(); const categoryMiddleList = extractCategoryMiddleList(records); return categoryMiddleList.map((categoryMiddle) => ({ category_middle: encodeURIComponent(categoryMiddle), })); }
    • 각 범죄 유형에 대해 generateMetadata 실행
    • 예: "살인기수", "강도", "사기" 등 38개 경로
    • 각 경로별로 고유한 메타데이터 생성
  2. 메타데이터 생성 예시

    /detail/살인기수
    → generateMetadata({ params: { category_middle: "살인기수" } })
    → { title: "살인기수 범죄 분석", description: "살인기수 범죄의... 총 594건..." }
    
    /detail/사기
    → generateMetadata({ params: { category_middle: "사기" } })
    → { title: "사기 범죄 분석", description: "사기 범죄의... 총 608,944건..." }
    
  3. 최종 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를 챙길 수 잇는지, 또 어떤 방식으로 챙기게 되는건지 알아보고 싶었습니다.

이제 다음 글에서는 버셀에 배포를 완료하고자 합니다.