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.tsx나 page.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:title과 twitter: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.tsx의 default title이 폴백으로 사용된다. 이 동작을 믿고 에러 처리를 notFound()로 일관되게 하면 타이틀 관리가 단순해진다.