← 전체 글로 돌아가기

Next.js

Next.js App Router에서 페이지별 title을 관리하는 방법

App Router의 Metadata API를 써서 전체 사이트 타이틀 템플릿과 페이지별 타이틀을 깔끔하게 분리한 방법을 정리했다.

블로그를 Next.js App Router로 옮기면서 페이지마다 <title>을 따로 관리하는 게 생각보다 손이 갔다. Pages Router 시절에는 각 페이지에서 <Head> 컴포넌트를 직접 썼는데, App Router에서는 방식이 바뀌었다.

Metadata API 기본 구조

App Router에서는 layout.tsxpage.tsx에서 metadata 객체 또는 generateMetadata 함수를 export한다.

// app/layout.tsx — 사이트 전체 기본값
export const metadata: Metadata = {
  title: {
    default: 'turin\'s blog',
    template: '%s | turin\'s blog',
  },
  description: '개발 경험을 기록하는 블로그',
};

template을 지정하면 하위 페이지에서 title을 문자열로 내보낼 때 자동으로 템플릿이 적용된다.

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug);
  return {
    title: post.title, // 렌더링 결과: "포스트 제목 | turin's blog"
  };
}

홈 페이지는 template 적용을 피해야 한다

template이 설정된 상태에서 루트 page.tsx에 그냥 title: 'turin\'s blog'를 내보내면 "turin's blog | turin's blog"처럼 중복이 생긴다. absolute를 쓰면 템플릿을 무시한다.

// app/page.tsx
export const metadata: Metadata = {
  title: {
    absolute: 'turin\'s blog', // 템플릿 무시
  },
};

OG title과 Twitter title

title을 설정하면 og:titletwitter:title도 같은 값이 자동으로 들어간다. 별도로 다른 텍스트를 쓰고 싶을 때는 명시적으로 지정한다.

export const metadata: Metadata = {
  title: '짧은 페이지 제목',
  openGraph: {
    title: 'SNS에서 보여질 더 길고 설명적인 제목',
  },
};

확인 방법

curl -s https://yourblog.com/blog/some-post | grep -i '<title>'
# 또는
curl -s https://yourblog.com/blog/some-post | grep 'og:title'

빌드 후에 직접 HTML을 확인하는 게 가장 확실하다. 개발 서버와 프로덕션 빌드에서 메타데이터 처리가 미묘하게 다를 수 있기 때문이다.

동적 경로에서 fallback 처리

존재하지 않는 슬러그로 접근했을 때 generateMetadata가 null을 받으면 layout.tsxdefault title이 폴백으로 사용된다. 이 동작을 믿고 에러 처리를 notFound()로 일관되게 하면 타이틀 관리가 단순해진다.