Develop

Next.js App Router 메타데이터 완벽 가이드 — generateMetadata로 SEO 최적화하는 방법

Next.js App Router에서는 Head 컴포넌트 대신 Metadata API를 써야 합니다. layout.tsx 전역 설정부터 포스트별 generateMetadata, robots.ts와 sitemap.ts까지 가볍게 정리해 보았습니다.

Next.jsApp RouterSEO메타데이터generateMetadata
Next.js App Router의 generateMetadata 함수 코드 예시 — slug 파라미터로 포스트별 동적 메타데이터를 생성하는 구조
  • ·Next.js App Router Metadata API: 13.2 버전부터 도입
  • ·generateMetadata는 서버에서 실행되며 async/await 사용 가능
  • ·layout.tsx의 title template: '%s | 사이트명' 형태로 하위 페이지 타이틀 자동 조합
  • ·app/robots.ts와 app/sitemap.ts: 코드로 robots.txt와 sitemap.xml을 자동 생성
Pages Router 기반 프로젝트를 App Router로 마이그레이션하면서 메타데이터 설정 방식이 완전히 달라졌다는 걸 처음에 몰랐습니다. 기존 방식대로 app/layout.tsx에 next/head를 import했다가 서버 컴포넌트에서 경고가 뜨고 나서야 Metadata API로 부랴부랴 바꿨습니다. 하지만 generateMetadata를 쓰기 시작하면서 블로그 포스트 페이지마다 타이틀과 Open Graph 이미지가 자동으로 잡히는 걸 보고, 역시 진작에 마이그레이션하길 잘했다는 생각이 들었습니다.

1. App Router에서 달라진 메타데이터 개념

왜 Next.js App Router는 기존의 Head 컴포넌트를 버렸을까

기존 Pages Router에서 개발할 때는 next/head에서 제공하는 Head 컴포넌트를 쓰는 게 당연했습니다. 페이지마다 패키지를 가져와서 <Head> 태그를 열고 <title>이나 <meta> 태그를 직접 손으로 타이핑하는 방식이었죠. 하지만 Next.js가 App Router로 오면서 서버 컴포넌트(Server Component) 중심 아키텍처로 바뀌자 이 방식이 완전히 꼬여버렸습니다. 서버 컴포넌트는 서버에서 미리 렌더링되어 정적 HTML 구조로 내려오는데, 클라이언트 사이드 동작에 의존하던 next/head 컴포넌트를 서버 컴포넌트 안에서 그대로 쓰면 에러가 나거나 아예 렌더링에 반영되지 않는 문제가 터졌습니다. 저도 처음 마이그레이션할 때 아무 생각 없이 app/layout.tsx나 개별 page.tsxnext/head를 가져다 썼다가 빌드 타임에 빨간 경고창이 주르륵 뜨는 걸 보고 당황했던 기억이 있습니다. Next.js는 이 문제를 해결하기 위해 13.2 버전부터 새로운 Metadata API를 도입했습니다. 이제는 컴포넌트를 렌더링하는 대신, 파일 상단에서 metadata 객체를 정의해서 내보내거나(export), 동적인 값이 필요하면 generateMetadata 함수를 정의해두기만 하면 됩니다. 이렇게 하니 HTML의 head 태그 구조가 서버 단에서 안전하게 조립되어 내려가므로 속도도 빠르고, 무엇보다 Metadata 타입을 지정해둘 수 있어서 오타가 나거나 엉뚱한 값을 넣었을 때 에러를 빌드 타임에 미리 잡을 수 있다는 점이 진짜 편리합니다.

정적 metadata 선언과 generateMetadata 동적 생성의 쓰임새 차이

App Router에서 메타데이터를 선언하는 법은 단순하게 metadata 객체를 정적으로 채워서 내보내는 방식과, generateMetadata라는 비동기 함수를 만들어서 내보내는 동적 방식 두 가지로 나뉩니다. 소개 페이지나 랜딩 페이지처럼 URL 주소나 데이터와 무관하게 항상 고정된 정보만 노출하는 페이지라면 export const metadata: Metadata 형태로 간단하게 객체만 정의해주면 됩니다. 반면, 데이터베이스나 외부 API에서 글 정보를 실시간으로 읽어와 보여주는 블로그 상세 페이지 같은 곳에서는 동적 선언이 필수입니다. 이때 사용하는 generateMetadata 함수는 현재 페이지의 URL 파라미터(params)나 쿼리 스트링(searchParams)을 인자로 받아서 호출됩니다. 이 인자값을 활용해 포스트의 상세 내용을 서버 단에서 fetch하고, 가져온 실제 제목과 요약본을 조합해서 Metadata 형태로 리턴해주는 구조입니다. 여기서 흔히 하는 걱정 중 하나가 '메타데이터 뽑아올 때 한 번, 본문 컴포넌트 렌더링할 때 또 한 번, 이렇게 데이터를 두 번 요청해서 DB나 네트워크에 무리가 가지 않을까?' 하는 점입니다. 다행히 Next.js는 자체적인 fetch 캐싱 메커니즘을 내장하고 있어서, 동일한 페이지 렌더링 라이프사이클 내에서 일어나는 같은 API 호출은 자동으로 캐싱 처리되어 실제 요청은 딱 한 번만 나갑니다. 덕분에 성능 고민 없이 안심하고 호출 코드를 작성할 수 있습니다.

2. layout.tsx를 활용한 글로벌 메타데이터 구성

App Router의 layout.tsx에서 기본 메타데이터 정의하기

글로벌 서비스의 기본 뼈대가 되는 메타데이터는 루트 레이아웃인 app/layout.tsx에서 잡아주면 됩니다. 보통 사이트 전체에 공통으로 들어갈 파비콘, 언어 설정, 그리고 기본 타이틀 형식을 정해두는데, 이때 타이틀의 template 기능을 활용하는 걸 적극 추천합니다. title: { default: '내 개발 블로그', template: '%s | 내 개발 블로그' } 형태로 설정해두면 하위 페이지에서 굳이 매번 뒤에 블로그 이름을 덧붙이지 않고 { title: '포스트 제목' }만 리턴해줘도 Next.js가 중간의 %s 자리에 알아서 치환하여 '포스트 제목 | 내 개발 블로그'라는 깔끔한 타이틀을 브라우저 탭에 띄워줍니다. 다만 주의할 점이 하나 있는데, 레이아웃에 선언된 메타데이터는 하위 폴더의 레이아웃이나 페이지로 내려가면서 병합(Merge)된다는 점입니다. 이 병합 과정이 때로는 예상치 못하게 작동해서 특정 페이지에서 타이틀이 깨지거나 설명글이 빈 상태로 올라가는 경우가 있습니다. 특히 검색 로봇 색인을 제어하는 robots 필드나 검색 노출용 description 등은 기본 설정만 믿고 방치하기보다는, 검색 노출이 정말 중요한 개별 상세 페이지에서 확실하게 오버라이드(Override)해주는 것이 장기적인 SEO 관점에서 든든하고 안전합니다.

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    default: '사이트명',
    template: '%s | 사이트명',
  },
  description: '사이트 설명을 160자 이내로 작성',
  robots: { index: true, follow: true },
  openGraph: {
    type: 'website',
    siteName: '사이트명',
  },
}

공유 화면을 책임지는 Open Graph 메타데이터 세팅하기

카카오톡이나 슬랙, 페이스북 같은 곳에 블로그 글 링크를 복사해서 보냈을 때 카드 형태로 썸네일 이미지와 제목, 설명이 예쁘게 뜨는 화면을 본 적이 있을 것입니다. 이 미리보기를 제어하는 스펙이 바로 오픈 그래프(Open Graph, OG) 프로토콜입니다. 메타데이터 객체의 openGraph 필드를 선언해서 세팅할 수 있으며, 이 부분을 잘 작성해두면 SNS를 통한 트래픽 유입률이 확실히 올라갑니다. 가장 기본이 되는 권장 크기는 1200x630 픽셀인데, 이 규격보다 너무 작거나 비율이 맞지 않는 이미지를 지정하면 공유했을 때 썸네일이 잘리거나 아예 표시되지 않아 엄청 투박해 보입니다. 저 같은 경우에는 초기에 썸네일 이미지 파일 경로를 상대 경로로 적었다가 외부 플랫폼에서 크롤링해갈 때 이미지가 아예 깨져서 나오는 삽질을 한 적이 있습니다. 오픈 그래프 이미지는 반드시 https://로 시작하는 절대 경로(Absolute URL)로 지정해 줘야 외부 검색 로봇이나 크롤러들이 에러 없이 올바르게 가져갈 수 있습니다. 또한 Next.js는 opengraph-image.tsx라는 특수 파일을 라우트 폴더 내에 배치해서 동적으로 이미지 내에 텍스트를 합성하는 기능도 기본 지원하므로, 정적 썸네일을 만들기 귀찮거나 동적 텍스트 합성이 필요할 때 활용하면 개발 공수를 크게 덜어낼 수 있습니다.

3. 동적 데이터 매핑과 검색 엔진 최적화(SEO)

generateMetadata 함수로 개별 블로그 포스트의 메타데이터 동적 생성하기

실제 프로젝트를 빌드하고 배포할 때 가장 핵심이 되는 부분은 블로그 상세 페이지인 app/[locale]/blog/[slug]/page.tsxgenerateMetadata를 적용하는 작업입니다. 이 함수는 비동기(async)로 동작하기 때문에 함수 안에서 필요한 데이터를 직접 await해서 가져올 수 있습니다. 대표적인 구현 패턴은 파일 매개변수(params)에서 추출한 slug 값을 가지고 백엔드 API나 로컬 JSON 파일에서 글 정보를 읽어온 뒤, 타이틀과 본문 요약을 반환하는 형태입니다. 여기서 유용한 팁은 존재하지 않는 엉뚱한 슬러그로 사용자가 접근했을 때의 예외 처리입니다. generateMetadata 함수 내부에서도 Next.js의 notFound() 함수를 실행할 수 있으므로, 데이터를 조회해서 결과가 없다면 바로 404 페이지로 리다이렉트 시켜버리면 깔끔합니다. 또한, 다국어 지원을 위해 next-intl 같은 라이브러리를 붙여 쓰고 있다면, 로케일 매개변수(locale) 정보도 여기서 함께 파라미터로 받아 다국어 캐노니컬(canonical) 링크 설정을 해줘야 중복 컨텐츠 페널티를 피할 수 있습니다. 처음에는 주소 뒤에 슬래시 하나 붙는 차이(Trailing Slash) 때문에 구글 서치 콘솔에 색인이 누락되거나 이중 등록되는 버그를 겪기도 했는데, 캐노니컬 설정을 URL 규칙에 맞춰 꼼꼼히 잡아준 뒤에야 비로소 구글 검색 엔진에 정상적으로 수집되는 성과를 낼 수 있었습니다.

import type { Metadata } from 'next'

export async function generateMetadata(
  { params }: { params: { slug: string } }
): Promise<Metadata> {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.description,
    openGraph: {
      title: post.title,
      description: post.description,
      images: [{ url: post.thumbnail, width: 1200, height: 630 }],
    },
  }
}

robots.ts와 sitemap.ts로 검색 로봇에게 친절한 SEO 구축하기

메타태그를 열심히 달아도 검색 엔진의 로봇들이 우리 사이트에 들어와 보지도 않는다면 아무 소용이 없습니다. 그래서 필요한 게 바로 크롤링 가이드인 robots.txt와 사이트의 모든 주소 지도가 담긴 sitemap.xml입니다. 예전에는 빌드 스크립트를 따로 돌리거나 외부 생성 도구를 써서 빌드 결과물 폴더에 static하게 복사해 넣곤 했는데, Next.js App Router에서는 이 작업을 아주 우아하게 처리해줍니다. app 디렉토리 바로 밑에 robots.tssitemap.ts라는 특수 파일을 생성하고 정해진 타입의 객체를 반환하도록 만들기만 하면 됩니다. 예를 들어 sitemap.ts에서는 우리가 로컬 JSON이나 DB에 보관하고 있는 모든 포스트 목록을 가져와서 [{ url: 'https://...', lastModified: new Date() }] 형태의 배열로 맵핑해 리턴해주면 빌드 시점에 자동으로 규격에 맞는 XML 파일이 빌드 아웃풋으로 튀어나옵니다. 실제로 내 블로그에 이 두 파일을 동적으로 연동해서 구글 서치 콘솔(Google Search Console)에 등록한 직후, 기존에는 새 글을 올리고 며칠씩 기다려야 겨우 구글에 노출되던 것이 이제는 빠르면 몇 시간 안에 바로 색인이 완료되어 상위 노출에 들어가는 경험을 했습니다. 검색 엔진 노출 속도가 더뎌 답답했던 개발자라면 무조건 도입해야 하는 강력한 설정입니다.

자주 묻는 질문

generateMetadata와 본문 컴포넌트에서 동일한 API를 각각 fetch하면 중복 호출이 일어나지 않나요?+

걱정하지 않으셔도 됩니다. Next.js는 기본적으로 fetch 함수를 확장(monkey-patch)하여 동일한 URL과 옵션의 요청은 자동으로 디듀프(Deduplicate) 및 캐싱 처리를 해줍니다. 따라서 두 곳에서 각각 호출해도 실제 네트워크 요청은 최초 한 번만 발생합니다.

로컬 환경(development)에서는 메타태그가 다 보이는데 구글 서치 콘솔에 등록이 잘 안 됩니다.+

구글 서치 콘솔이나 페이스북 디버거 등은 외부 크롤러가 사이트에 접근하여 HTML을 긁어가는 구조입니다. 로컬호스트(localhost) 환경은 외부에서 접근할 수 없으므로, Vercel이나 개인 서버에 실제로 배포하여 도메인을 연결한 상태에서 테스트를 진행하셔야 정상적으로 메타태그가 수집되는 것을 확인하실 수 있습니다.

관련 글