본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 10. 28. · 58 Views
Next.js Metadata API로 SEO 완벽 정복하기
Next.js 13 이상에서 제공하는 Metadata API를 활용하여 검색 엔진 최적화(SEO)를 구현하는 방법을 배웁니다. 정적 메타데이터부터 동적 생성까지, 실무에서 바로 적용할 수 있는 완벽한 가이드입니다.
목차
- Metadata_객체_정적_메타데이터
- generateMetadata_동적_메타데이터
- OpenGraph_소셜_미디어_최적화
- Twitter_Cards_트위터_최적화
- robots_검색_엔진_크롤링_제어
- 동적_OG_이미지_ImageResponse_API
- Sitemap_생성_자동화
- 다국어_메타데이터_alternates
1. Metadata 객체 정적 메타데이터
시작하며
여러분이 Next.js로 웹사이트를 만들고 구글에서 검색해봤는데, 제목이 "localhost:3000"으로 나오거나 설명이 아예 없는 경험을 해보셨나요? 이런 상황은 초보 개발자들이 가장 자주 겪는 문제 중 하나입니다.
검색 엔진은 여러분의 웹사이트가 어떤 내용인지 알기 위해 메타데이터를 읽습니다. 메타데이터가 없으면 검색 결과에서 제대로 표시되지 않고, 사용자들이 클릭할 가능성도 현저히 떨어집니다.
실제로 SEO 전문가들은 좋은 메타데이터만으로도 클릭률을 30-40% 향상시킬 수 있다고 말합니다. 바로 이럴 때 필요한 것이 Next.js의 Metadata API입니다.
App Router에서는 단 몇 줄의 코드로 전문적인 SEO 설정을 완성할 수 있습니다.
개요
간단히 말해서, Metadata 객체는 페이지의 메타 정보를 선언적으로 정의하는 방법입니다. Next.js 13 이전에는 Head 컴포넌트를 사용해 일일이 메타태그를 작성해야 했지만, App Router에서는 간단한 객체 하나로 모든 메타데이터를 관리할 수 있습니다.
이는 타입 안정성도 제공하여 오타나 잘못된 속성 사용을 컴파일 타임에 잡아낼 수 있습니다. 기존에는 _app.tsx나 _document.tsx를 수정하고, Head 컴포넌트를 import하고, 여러 파일을 오가며 작업했다면, 이제는 해당 페이지 파일에서 metadata 객체만 export하면 됩니다.
이 방식의 핵심 특징은 첫째, 파일 기반 구조로 각 페이지마다 독립적인 메타데이터를 가질 수 있고, 둘째, TypeScript 지원으로 자동완성과 타입 체크가 가능하며, 셋째, 서버 컴포넌트에서 직접 데이터를 fetch해서 메타데이터를 생성할 수 있다는 점입니다. 이러한 특징들이 개발 속도와 코드 품질을 동시에 향상시킵니다.
코드 예제
// app/page.tsx
import type { Metadata } from 'next'
// 정적 메타데이터 export
export const metadata: Metadata = {
title: 'Next.js SEO 가이드 | 완벽한 검색 최적화',
description: 'Next.js Metadata API를 활용한 SEO 최적화 완벽 가이드입니다. 검색 엔진에서 상위 노출되는 방법을 배워보세요.',
keywords: ['Next.js', 'SEO', 'Metadata', '검색 최적화'],
authors: [{ name: '홍길동', url: 'https://example.com' }],
creator: '홍길동',
publisher: 'Example Publisher',
// viewport, themeColor 등 추가 설정 가능
}
export default function HomePage() {
return <main>홈페이지 콘텐츠</main>
}
설명
이것이 하는 일: Next.js는 export된 metadata 객체를 읽어서 자동으로 HTML의 <head> 태그 안에 적절한 메타태그들을 생성합니다. 여러분은 복잡한 HTML을 작성할 필요 없이, 간단한 JavaScript 객체만 정의하면 됩니다.
첫 번째로, title과 description은 검색 엔진 결과 페이지(SERP)에서 가장 중요한 요소입니다. title은 검색 결과의 파란색 제목으로 표시되고, description은 그 아래 회색 설명 텍스트로 나타납니다.
구글은 title을 60자 이내, description을 155자 이내로 권장하는데, 이를 넘으면 "..."으로 잘려서 표시됩니다. 그래서 핵심 정보는 앞부분에 배치하는 것이 중요합니다.
그 다음으로, keywords는 과거에는 중요했지만 현재는 구글이 공식적으로 무시한다고 발표했습니다. 하지만 네이버나 다음 같은 국내 검색 엔진에서는 여전히 참고하므로, 국내 시장을 타겟으로 한다면 포함하는 것이 좋습니다.
authors와 creator는 저작권과 신뢰성을 나타내는 지표로 활용됩니다. 마지막으로, 이 metadata 객체는 페이지가 렌더링될 때 서버에서 처리됩니다.
즉, 클라이언트로 전송되기 전에 이미 완성된 HTML에 메타태그가 포함되어 있어서, 검색 엔진 크롤러가 JavaScript를 실행하지 않아도 정보를 읽을 수 있습니다. 이것이 바로 SSR(Server-Side Rendering)의 SEO 이점입니다.
여러분이 이 코드를 사용하면 검색 엔진에서 페이지를 정확하게 인식하고, 검색 결과에 매력적으로 표시되며, 클릭률(CTR)을 크게 향상시킬 수 있습니다. 또한 코드 유지보수가 쉬워지고, 팀원들과 협업할 때도 일관된 방식으로 메타데이터를 관리할 수 있습니다.
실전 팁
💡 title은 페이지마다 unique하게 설정하되, 브랜드명을 뒤에 붙여서 일관성을 유지하세요. 예: "기능명 | 사이트명"
💡 description에는 행동 유도 문구(Call-to-Action)를 포함하면 클릭률이 올라갑니다. "지금 알아보세요", "무료로 시작하기" 같은 표현을 활용하세요.
💡 metadata 객체는 레이아웃 파일에서도 정의 가능하며, 하위 페이지의 metadata와 자동으로 병합됩니다. 공통 정보는 layout.tsx에 배치하세요.
💡 Next.js는 metadata의 순서를 최적화하여 배치하므로, 여러분이 순서를 걱정할 필요가 없습니다. 가독성 좋은 순서로 작성하면 됩니다.
💡 개발 중에는 브라우저 개발자 도구의 Elements 탭에서 <head>를 확인하여 메타태그가 제대로 생성되었는지 검증하세요.
2. generateMetadata 동적 메타데이터
시작하며
여러분이 블로그 포스트나 상품 상세 페이지를 만들 때, 각 페이지마다 다른 제목과 설명을 보여줘야 하는 상황을 생각해보세요. 정적 metadata로는 이런 동적 콘텐츠를 처리할 수 없습니다.
예를 들어, 10,000개의 상품이 있는 쇼핑몰이라면 각 상품마다 고유한 메타데이터가 필요합니다. 하지만 10,000개의 파일을 만들 수는 없는 노릇입니다.
또한 상품 정보가 변경될 때마다 메타데이터도 자동으로 업데이트되어야 합니다. 바로 이럴 때 필요한 것이 generateMetadata 함수입니다.
이 함수를 사용하면 URL 파라미터나 데이터베이스에서 가져온 데이터를 기반으로 동적으로 메타데이터를 생성할 수 있습니다.
개요
간단히 말해서, generateMetadata는 비동기 함수로 동적 데이터를 기반으로 메타데이터를 생성하는 방법입니다. 이 함수는 페이지가 렌더링되기 전에 서버에서 실행되며, URL 파라미터(params), 검색 쿼리(searchParams), 부모 메타데이터 등에 접근할 수 있습니다.
실무에서는 데이터베이스 조회, API 호출, 파일 시스템 읽기 등 거의 모든 비동기 작업을 수행할 수 있어 매우 강력합니다. 기존에는 getServerSideProps에서 데이터를 가져오고, Head 컴포넌트에 props를 전달하고, 복잡한 로직을 작성해야 했다면, 이제는 generateMetadata 함수 하나로 모든 것을 처리할 수 있습니다.
이 함수의 핵심 특징은 첫째, 자동 중복 제거(deduplication)로 동일한 데이터 요청이 여러 번 발생해도 한 번만 실행되며, 둘째, 스트리밍 방식으로 메타데이터가 먼저 전송되어 SEO에 유리하고, 셋째, 타입 안정성을 제공하여 params와 반환 타입이 모두 체크된다는 점입니다. 이러한 특징들이 성능과 개발 경험을 동시에 개선합니다.
코드 예제
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
interface Props {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
// 동적 메타데이터 생성 함수
export async function generateMetadata({ params }: Props): Promise<Metadata> {
// 데이터베이스나 API에서 데이터 fetch
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return {
title: `${post.title} | My Blog`,
description: post.excerpt,
authors: [{ name: post.author }],
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
export default async function PostPage({ params }: Props) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return <article>{post.content}</article>
}
설명
이것이 하는 일: generateMetadata 함수는 페이지 컴포넌트가 렌더링되기 전에 서버에서 먼저 실행되어, 동적 데이터를 기반으로 Metadata 객체를 생성합니다. 이렇게 생성된 메타데이터는 HTML의 <head>에 삽입되어 검색 엔진과 소셜 미디어에 제공됩니다.
첫 번째로, 함수의 params 인자를 통해 동적 라우트 세그먼트에 접근할 수 있습니다. 예를 들어 /blog/nextjs-seo라는 URL이라면 params.slug는 "nextjs-seo"가 됩니다.
이 값을 사용해 데이터베이스나 API에서 해당 포스트의 정보를 가져옵니다. Next.js는 이 fetch 요청을 자동으로 캐싱하므로, 같은 페이지에서 여러 번 호출해도 실제로는 한 번만 실행됩니다.
그 다음으로, 가져온 데이터를 사용해 Metadata 객체를 구성합니다. post.title, post.excerpt 같은 실제 콘텐츠 데이터가 메타데이터에 반영되므로, 검색 엔진이 정확한 정보를 인덱싱할 수 있습니다.
특히 openGraph 필드를 추가하면 소셜 미디어에서 공유될 때 이미지와 함께 보기 좋게 표시됩니다. 마지막으로, 중요한 최적화 포인트가 있습니다.
generateMetadata와 페이지 컴포넌트에서 같은 데이터를 fetch하더라도, Next.js는 Request Memoization을 통해 실제로는 한 번만 요청합니다. 즉, 위 코드에서 fetch가 두 번 나타나지만 실제 네트워크 요청은 한 번만 발생합니다.
이는 자동으로 처리되므로 개발자는 코드를 깔끔하게 유지할 수 있습니다. 여러분이 이 코드를 사용하면 수천, 수만 개의 동적 페이지에 대해 자동으로 최적화된 메타데이터를 생성할 수 있습니다.
또한 데이터가 업데이트되면 메타데이터도 자동으로 반영되어, 항상 최신 정보를 검색 엔진에 제공할 수 있습니다. 코드 중복도 없고, 성능도 뛰어나며, 유지보수도 쉬운 완벽한 솔루션입니다.
실전 팁
💡 generateMetadata에서 fetch한 데이터는 자동으로 캐싱되므로, 페이지 컴포넌트에서 똑같은 fetch를 호출해도 됩니다. 코드 중복을 걱정하지 마세요.
💡 데이터를 찾을 수 없는 경우(404)에는 notFound() 함수를 호출하여 404 페이지로 리다이렉트하세요. 이것이 SEO에도 좋습니다.
💡 부모 메타데이터를 확장하려면 세 번째 파라미터 parent를 사용하세요. await parent로 부모 메타데이터를 가져와 병합할 수 있습니다.
💡 generateMetadata는 서버에서만 실행되므로, 데이터베이스 직접 조회, 파일 시스템 접근 등 서버 전용 API를 자유롭게 사용할 수 있습니다.
💡 타임아웃을 설정하여 외부 API 호출이 너무 오래 걸리면 기본 메타데이터를 반환하도록 처리하세요. 사용자 경험이 중요합니다.
3. OpenGraph 소셜 미디어 최적화
시작하며
여러분이 열심히 작성한 블로그 글을 페이스북이나 카카오톡에 공유했는데, 이미지도 없고 제목이 이상하게 나와서 당황한 경험 있으신가요? 이는 OpenGraph 메타태그가 제대로 설정되지 않았기 때문입니다.
소셜 미디어 플랫폼들은 일반 메타태그가 아닌 OpenGraph 프로토콜을 사용해 콘텐츠를 표시합니다. 페이스북이 개발한 이 프로토콜은 현재 거의 모든 소셜 미디어에서 표준으로 사용됩니다.
통계에 따르면, 이미지가 포함된 공유 링크는 그렇지 않은 경우보다 클릭률이 94%나 높다고 합니다. 바로 이럴 때 필요한 것이 OpenGraph 메타데이터입니다.
Next.js의 Metadata API는 OpenGraph 설정을 매우 간편하게 만들어줍니다.
개요
간단히 말해서, OpenGraph는 소셜 미디어에서 URL이 공유될 때 표시되는 정보를 제어하는 프로토콜입니다. 웹페이지를 페이스북, 카카오톡, 슬랙, 디스코드 등에 공유하면, 플랫폼은 og:title, og:description, og:image 같은 OpenGraph 메타태그를 읽어서 멋진 카드 형태로 보여줍니다.
이는 단순한 URL 링크보다 훨씬 더 매력적이고, 사용자들의 클릭을 유도하는 데 효과적입니다. 기존에는 <meta property="og:title" content="..." /> 같은 HTML 태그를 일일이 작성해야 했다면, Next.js에서는 metadata.openGraph 객체만 정의하면 자동으로 모든 태그가 생성됩니다.
핵심 특징은 첫째, 타입 안전성으로 잘못된 속성명을 사용할 수 없고, 둘째, 자동 URL 해석으로 상대 경로를 절대 경로로 변환해주며, 셋째, 이미지 크기 자동 감지로 og:image:width와 height를 자동으로 추가한다는 점입니다. 이러한 특징들이 개발 시간을 대폭 단축시켜줍니다.
코드 예제
// app/products/[id]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const product = await getProduct(params.id)
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
url: `https://myshop.com/products/${params.id}`,
siteName: 'My Shop',
images: [
{
url: product.imageUrl,
width: 1200,
height: 630,
alt: product.name,
},
],
locale: 'ko_KR',
type: 'website',
},
}
}
설명
이것이 하는 일: openGraph 객체에 정의된 정보는 <meta property="og:*"> 태그들로 변환되어 HTML head에 삽입됩니다. 소셜 미디어 플랫폼들은 이 태그를 읽어서 링크 미리보기를 생성합니다.
첫 번째로, images 배열은 가장 중요한 요소입니다. 페이스북과 링크드인은 1200x630 픽셀(1.91:1 비율)을 권장하며, 이 크기로 맞추면 대부분의 플랫폼에서 잘 표시됩니다.
width와 height를 명시하면 플랫폼이 이미지를 다운로드하기 전에 레이아웃을 미리 계산할 수 있어 사용자 경험이 개선됩니다. alt 텍스트는 이미지가 로드되지 않을 때 대체 텍스트로 사용되며, 접근성에도 중요합니다.
그 다음으로, url 필드는 정규 URL(canonical URL)을 지정합니다. 같은 콘텐츠가 여러 URL로 접근 가능한 경우(예: 쿼리 파라미터가 있는 경우), 대표 URL을 명시하면 소셜 미디어에서 공유 카운트가 분산되지 않고 하나로 집계됩니다.
siteName은 콘텐츠의 출처를 나타내며, 사용자들에게 신뢰감을 줍니다. 마지막으로, type 필드는 콘텐츠의 종류를 지정합니다.
'website'는 일반 웹페이지, 'article'은 블로그 포스트나 뉴스 기사, 'product'는 전자상거래 상품에 사용됩니다. type에 따라 추가로 제공할 수 있는 메타데이터가 달라집니다.
예를 들어 'article' 타입은 publishedTime, author 같은 필드를 추가할 수 있습니다. 여러분이 이 코드를 사용하면 소셜 미디어에서 공유될 때 전문적이고 매력적인 링크 미리보기가 생성됩니다.
이는 클릭률을 크게 향상시키고, 트래픽 증가로 이어집니다. 또한 브랜드 이미지도 개선되어, 사용자들이 여러분의 콘텐츠를 더 신뢰하게 됩니다.
실전 팁
💡 OpenGraph 이미지는 반드시 절대 URL(https://...)을 사용해야 합니다. 상대 경로는 작동하지 않습니다.
💡 페이스북 공유 디버거(https://developers.facebook.com/tools/debug/)에서 URL을 입력하면 OpenGraph 태그가 제대로 설정되었는지 확인할 수 있습니다.
💡 이미지 파일은 8MB 이하로 유지하세요. 너무 크면 일부 플랫폼에서 로드되지 않습니다. WebP 형식을 사용하면 용량을 줄일 수 있습니다.
💡 title과 description은 OpenGraph용으로 별도 지정할 수 있습니다. 일반 메타태그보다 짧게 작성하면 소셜 미디어에 최적화됩니다.
💡 여러 이미지를 배열로 제공하면 플랫폼에 따라 다른 이미지를 선택하거나 슬라이드로 표시할 수 있습니다.
4. Twitter Cards 트위터 최적화
시작하며
여러분이 트위터(현재는 X)에 링크를 공유했는데, 작은 이미지 썸네일만 나오거나 아예 이미지가 안 나오는 경우가 있나요? 트위터는 OpenGraph도 읽지만, 자체적인 Twitter Cards 메타태그를 더 우선적으로 사용합니다.
트위터는 전 세계적으로 영향력 있는 플랫폼이며, 특히 개발자 커뮤니티에서 매우 활발하게 사용됩니다. 좋은 Twitter Card 설정은 리트윗과 좋아요를 늘리고, 여러분의 콘텐츠가 더 많은 사람들에게 도달하도록 도와줍니다.
바로 이럴 때 필요한 것이 Twitter Cards 메타데이터입니다. Next.js는 twitter 필드를 통해 이를 간단하게 설정할 수 있습니다.
개요
간단히 말해서, Twitter Cards는 트위터에서 링크가 공유될 때 표시되는 리치 미디어 카드를 제어하는 메타데이터입니다. 트위터는 여러 카드 타입을 지원합니다.
'summary'는 작은 정사각형 이미지, 'summary_large_image'는 큰 직사각형 이미지, 'app'은 모바일 앱 다운로드, 'player'는 비디오나 오디오 재생을 위한 타입입니다. 대부분의 경우 'summary_large_image'를 사용하면 시각적으로 가장 임팩트가 큽니다.
기존에는 <meta name="twitter:card" content="..." /> 같은 태그를 수동으로 작성해야 했다면, Next.js에서는 metadata.twitter 객체로 모든 것을 관리할 수 있습니다. 핵심 특징은 첫째, OpenGraph와 자동 폴백 기능이 있어 twitter 필드를 생략하면 openGraph 값을 사용하고, 둘째, creator와 site 필드로 트위터 계정을 연결할 수 있으며, 셋째, 카드 타입별로 최적화된 이미지 크기를 사용할 수 있다는 점입니다.
이러한 특징들이 트위터에서의 도달 범위를 극대화합니다.
코드 예제
// app/articles/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const article = await getArticle(params.slug)
return {
title: article.title,
description: article.excerpt,
openGraph: {
title: article.title,
description: article.excerpt,
images: [article.coverImage],
},
twitter: {
card: 'summary_large_image',
title: article.title,
description: article.excerpt,
images: [article.coverImage],
creator: '@myhandle',
site: '@mysite',
},
}
}
설명
이것이 하는 일: twitter 객체에 정의된 정보는 <meta name="twitter:*"> 태그들로 변환되어 HTML에 삽입됩니다. 트위터는 이 태그를 읽어서 트윗에 첨부되는 카드를 렌더링합니다.
첫 번째로, card 필드는 카드의 레이아웃을 결정합니다. 'summary'는 작은 150x150 픽셀 썸네일을 왼쪽에 표시하고, 'summary_large_image'는 큰 이미지를 상단에 배치합니다.
데이터에 따르면 large_image 타입이 일반 summary보다 2-3배 높은 클릭률을 보여줍니다. 시각적으로 풍부한 콘텐츠일수록 사용자의 관심을 끌기 때문입니다.
그 다음으로, creator와 site 필드는 트위터 계정을 연결합니다. creator는 콘텐츠 작성자의 트위터 핸들(@로 시작)을 지정하고, site는 웹사이트의 공식 트위터 계정을 나타냅니다.
이렇게 설정하면 카드 하단에 "by @creator"가 표시되고, 클릭하면 해당 트위터 프로필로 이동합니다. 이는 작성자와 독자를 연결하는 강력한 방법입니다.
마지막으로, twitter 객체를 생략하면 Next.js는 자동으로 openGraph 값을 폴백으로 사용합니다. 즉, openGraph만 설정해도 트위터에서 어느 정도 작동하지만, 트위터 특화 설정을 추가하면 더 나은 결과를 얻을 수 있습니다.
특히 creator 필드는 OpenGraph에 없는 트위터 전용 기능이므로, 설정하면 추가적인 이점을 얻습니다. 여러분이 이 코드를 사용하면 트위터에서 콘텐츠가 공유될 때 전문적이고 클릭하고 싶은 카드가 생성됩니다.
이는 리트윗과 인게이지먼트를 증가시키고, 궁극적으로 더 많은 트래픽과 팔로워를 얻는 데 도움이 됩니다. 특히 개발자 커뮤니티에서 활동한다면 필수적인 설정입니다.
실전 팁
💡 'summary_large_image'용 이미지는 최소 300x157 픽셀, 권장 크기는 1200x628 픽셀입니다. OpenGraph 이미지와 공유해도 됩니다.
💡 트위터 카드 유효성 검사기(https://cards-dev.twitter.com/validator)에서 URL을 입력하면 카드가 어떻게 보이는지 미리 확인할 수 있습니다.
💡 twitter 필드를 생략하면 OpenGraph로 폴백되지만, creator 같은 트위터 전용 기능은 사용할 수 없으니 명시적으로 설정하는 것을 권장합니다.
💡 비디오 콘텐츠라면 'player' 카드 타입을 사용해 트위터 내에서 바로 재생할 수 있게 만들 수 있습니다. 인게이지먼트가 크게 향상됩니다.
💡 이미지는 5MB 이하여야 하며, GIF는 지원되지만 애니메이션은 재생되지 않습니다. MP4를 사용하려면 'player' 타입을 사용하세요.
5. robots 검색 엔진 크롤링 제어
시작하며
여러분이 개발 중인 스테이징 사이트나 관리자 페이지가 구글 검색 결과에 나타나서 당황한 경험이 있나요? 또는 특정 페이지는 검색 결과에 나오지 않기를 원하는 경우가 있을 겁니다.
검색 엔진 크롤러는 기본적으로 모든 페이지를 인덱싱하려고 합니다. 하지만 모든 페이지가 검색 결과에 나타나야 하는 것은 아닙니다.
로그인 페이지, 결제 페이지, 개인정보 페이지 등은 검색 결과에서 제외되어야 하며, 이는 보안과 사용자 경험 측면에서도 중요합니다. 바로 이럴 때 필요한 것이 robots 메타태그입니다.
Next.js의 Metadata API는 robots 설정을 매우 간단하게 만들어줍니다.
개요
간단히 말해서, robots 메타데이터는 검색 엔진 크롤러에게 페이지를 어떻게 처리해야 하는지 지시하는 설정입니다. robots 태그는 여러 지시어를 지원합니다.
'index'는 페이지를 검색 결과에 포함시키고, 'noindex'는 제외시킵니다. 'follow'는 페이지의 링크를 따라가도록 허용하고, 'nofollow'는 차단합니다.
이 외에도 'noarchive'(캐시 금지), 'nosnippet'(설명 미리보기 금지), 'noimageindex'(이미지 인덱싱 금지) 등 세밀한 제어가 가능합니다. 기존에는 <meta name="robots" content="noindex, nofollow" />를 직접 작성해야 했다면, Next.js에서는 metadata.robots 객체나 간단한 문자열로 설정할 수 있습니다.
핵심 특징은 첫째, 페이지별로 독립적인 설정이 가능하여 일부 페이지만 선택적으로 제외할 수 있고, 둘째, 타입 안전성으로 잘못된 지시어를 사용할 수 없으며, 셋째, googleBot 같은 특정 크롤러에 대한 별도 설정도 가능하다는 점입니다. 이러한 특징들이 SEO를 정밀하게 제어할 수 있게 합니다.
코드 예제
// app/admin/page.tsx - 관리자 페이지는 인덱싱 금지
export const metadata = {
title: '관리자 대시보드',
robots: {
index: false,
follow: false,
nocache: true,
},
}
// app/blog/[slug]/page.tsx - 블로그는 인덱싱 허용
export const metadata = {
title: '블로그 포스트',
robots: {
index: true,
follow: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
}
설명
이것이 하는 일: robots 설정은 <meta name="robots" content="..."> 태그로 변환되어 검색 엔진 크롤러에게 지시사항을 전달합니다. 크롤러는 이 태그를 읽고 페이지를 어떻게 처리할지 결정합니다.
첫 번째로, index와 follow의 조합을 이해하는 것이 중요합니다. index: true, follow: true는 "이 페이지를 검색 결과에 포함시키고, 링크도 따라가라"는 의미로 일반적인 공개 페이지에 사용합니다.
index: false, follow: true는 "이 페이지는 검색 결과에서 제외하지만, 링크는 따라가라"로 로그인 페이지 같은 곳에 적합합니다. index: false, follow: false는 완전히 차단하는 것으로 관리자 페이지에 사용합니다.
그 다음으로, googleBot 필드를 사용하면 구글 크롤러에만 특별한 지시를 할 수 있습니다. 'max-image-preview'는 검색 결과에 표시되는 이미지 미리보기 크기를 제어하며, 'large'로 설정하면 큰 이미지를 보여줍니다.
'max-snippet'은 설명 텍스트의 최대 길이를 지정하며, -1은 제한 없음을 의미합니다. 이렇게 설정하면 구글 검색 결과에서 더 풍부한 미리보기를 제공할 수 있습니다.
마지막으로, nocache(또는 noarchive)는 검색 엔진이 페이지의 캐시된 버전을 저장하지 못하게 합니다. 빠르게 변하는 콘텐츠나 시간에 민감한 정보(주식 가격, 실시간 데이터 등)를 다루는 페이지에서 유용합니다.
사용자가 항상 최신 정보를 보도록 강제할 수 있습니다. 여러분이 이 코드를 사용하면 검색 엔진에 노출되어야 할 페이지와 그렇지 않은 페이지를 명확히 구분할 수 있습니다.
이는 SEO를 개선할 뿐만 아니라, 보안과 프라이버시도 강화합니다. 또한 크롤링 예산(crawl budget)을 최적화하여, 중요한 페이지에 크롤러의 리소스가 집중되도록 할 수 있습니다.
실전 팁
💡 개발/스테이징 환경에서는 환경 변수로 robots를 noindex로 자동 설정하여 실수로 인덱싱되는 것을 방지하세요.
💡 robots.txt 파일은 사이트 전체 규칙을 정의하고, robots 메타태그는 페이지별 규칙을 정의합니다. 둘 다 사용하면 더 정밀한 제어가 가능합니다.
💡 noindex를 설정해도 이미 인덱싱된 페이지는 즉시 사라지지 않습니다. 구글이 다시 크롤링할 때까지 기다리거나 Search Console에서 수동으로 제거 요청하세요.
💡 민감한 정보는 robots만으로 보호하지 말고, 반드시 인증(authentication)과 권한(authorization)을 구현하세요. robots는 권장사항일 뿐 강제력이 없습니다.
💡 all, none 같은 단축어를 사용할 수 있습니다. 'all'은 index와 follow를 모두 허용, 'none'은 모두 금지합니다.
6. 동적 OG 이미지 ImageResponse API
시작하며
여러분이 블로그 포스트가 수백 개인데, 각각에 대해 OpenGraph 이미지를 일일이 디자인하고 만드는 것은 불가능에 가깝죠? 또는 상품이 추가될 때마다 자동으로 멋진 공유 이미지가 생성되면 얼마나 좋을까요?
정적 이미지는 제작하는 데 시간이 많이 걸리고, 콘텐츠가 업데이트되면 이미지도 다시 만들어야 합니다. 특히 동적 콘텐츠가 많은 사이트에서는 수동으로 관리하는 것이 현실적으로 불가능합니다.
대형 뉴스 사이트나 전자상거래 플랫폼은 어떻게 수천 개의 페이지마다 고유한 이미지를 제공할까요? 바로 이럴 때 필요한 것이 Next.js의 ImageResponse API입니다.
이를 사용하면 JSX와 CSS로 동적으로 이미지를 생성할 수 있습니다.
개요
간단히 말해서, ImageResponse API는 React 컴포넌트를 PNG 이미지로 변환하여 동적 OG 이미지를 생성하는 기능입니다. 이 API는 Vercel이 개발한 Satori 라이브러리를 기반으로 하며, JSX를 SVG로 변환한 후 PNG로 래스터화합니다.
즉, 여러분은 HTML과 CSS를 작성하듯이 이미지를 디자인할 수 있고, 동적 데이터(제목, 작성자, 날짜 등)를 자동으로 이미지에 포함시킬 수 있습니다. Canvas API나 복잡한 이미지 처리 라이브러리 없이도 말이죠.
기존에는 Puppeteer로 브라우저를 띄워 스크린샷을 찍거나, Canvas로 픽셀을 직접 그리거나, 외부 서비스에 의존해야 했다면, 이제는 Next.js에서 제공하는 간단한 API로 모든 것을 해결할 수 있습니다. 핵심 특징은 첫째, Edge Runtime에서 실행되어 매우 빠르고 비용 효율적이며, 둘째, Flexbox CSS를 지원하여 레이아웃을 쉽게 만들 수 있고, 셋째, 커스텀 폰트를 로드하여 브랜드 일관성을 유지할 수 있다는 점입니다.
이러한 특징들이 동적 이미지 생성을 프로덕션에서 실용적으로 만들어줍니다.
코드 예제
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return new ImageResponse(
(
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', height: '100%',
backgroundColor: '#1e293b', padding: 60, color: 'white' }}>
<div style={{ fontSize: 72, fontWeight: 'bold' }}>{post.title}</div>
<div style={{ fontSize: 32, marginTop: 20, opacity: 0.8 }}>{post.excerpt}</div>
<div style={{ fontSize: 28, marginTop: 'auto' }}>by {post.author}</div>
</div>
),
{ ...size }
)
}
설명
이것이 하는 일: Next.js는 특별한 파일명 규칙(opengraph-image.tsx, twitter-image.tsx)을 인식하여, 해당 파일에서 export된 ImageResponse를 자동으로 OG 이미지로 사용합니다. 이미지는 요청 시점에 생성되고 캐싱됩니다.
첫 번째로, 파일 이름이 매우 중요합니다. opengraph-image.tsx는 OpenGraph 이미지를, twitter-image.tsx는 트위터 이미지를 생성합니다.
이 파일을 해당 라우트 디렉토리에 배치하면, Next.js가 자동으로 메타데이터에 이미지 URL을 추가합니다. 예를 들어 app/blog/[slug]/opengraph-image.tsx는 /blog/my-post/opengraph-image 경로에서 이미지를 제공합니다.
그 다음으로, ImageResponse 컴포넌트는 제한된 HTML/CSS만 지원합니다. Flexbox는 작동하지만 Grid는 안 되고, 대부분의 CSS 속성은 인라인 스타일로만 작성해야 합니다.
지원되는 속성 목록은 Satori 문서를 참고하세요. 이러한 제약에도 불구하고, 텍스트, 이미지, 그라디언트, 그림자 등을 사용해 충분히 멋진 디자인을 만들 수 있습니다.
마지막으로, 성능 최적화가 중요합니다. runtime = 'edge'를 설정하면 Edge Runtime에서 실행되어 전 세계 어디서나 빠르게 응답합니다.
size와 contentType을 export하면 Next.js가 이미지 메타데이터를 미리 알 수 있어 최적화에 도움이 됩니다. 생성된 이미지는 자동으로 캐싱되므로, 같은 페이지에 대한 반복 요청은 즉시 응답합니다.
여러분이 이 코드를 사용하면 수천 개의 페이지에 대해 고유하고 브랜드에 맞는 OG 이미지를 자동으로 생성할 수 있습니다. 디자이너의 도움 없이도 전문적인 공유 이미지를 만들 수 있고, 콘텐츠가 업데이트되면 이미지도 자동으로 반영됩니다.
이는 소셜 미디어에서의 클릭률을 크게 향상시키고, 브랜드 일관성도 유지해줍니다.
실전 팁
💡 커스텀 폰트를 사용하려면 fetch로 폰트 파일을 가져와 fonts 옵션에 전달하세요. 구글 폰트도 사용 가능합니다.
💡 이미지 생성은 상대적으로 비용이 높으므로, 프로덕션에서는 CDN 캐싱을 반드시 설정하세요. Vercel은 자동으로 캐싱해줍니다.
💡 디버깅 시 브라우저에서 /your-page/opengraph-image에 직접 접속하면 생성된 이미지를 확인할 수 있습니다.
💡 복잡한 레이아웃은 Flexbox의 justify-content와 align-items를 활용하면 쉽게 만들 수 있습니다. 웹 레이아웃과 동일하게 생각하세요.
💡 텍스트가 너무 길면 잘릴 수 있으므로, 제목은 60자 이내로 제한하는 로직을 추가하세요. ${text.slice(0, 60)}... 같은 처리를 하면 됩니다.
7. Sitemap 생성 자동화
시작하며
여러분이 사이트에 새로운 콘텐츠를 추가했는데, 구글에서 검색해도 나오지 않아서 답답했던 경험이 있나요? 검색 엔진은 여러분의 모든 페이지를 자동으로 찾아주지 않습니다.
특히 링크로 연결되지 않은 페이지나 깊숙이 숨겨진 페이지는 발견되기까지 오랜 시간이 걸립니다. Sitemap은 검색 엔진에게 "내 사이트에는 이런 페이지들이 있어요"라고 알려주는 지도입니다.
구글 Search Console에 sitemap을 제출하면, 크롤러가 효율적으로 모든 페이지를 찾아서 인덱싱할 수 있습니다. 대형 사이트일수록 sitemap은 필수적입니다.
바로 이럴 때 필요한 것이 Next.js의 Sitemap 생성 기능입니다. sitemap.ts 파일만 만들면 자동으로 sitemap.xml이 생성됩니다.
개요
간단히 말해서, Sitemap은 사이트의 모든 URL을 나열한 XML 파일로, 검색 엔진이 효율적으로 페이지를 발견하고 인덱싱하도록 돕습니다. Sitemap.xml 파일에는 각 URL의 주소, 마지막 수정 날짜, 변경 빈도, 우선순위 같은 정보가 포함됩니다.
검색 엔진은 이 정보를 사용해 어떤 페이지를 얼마나 자주 크롤링할지 결정합니다. 예를 들어 매일 업데이트되는 뉴스 페이지는 높은 우선순위를, 변경이 드문 정책 페이지는 낮은 우선순위를 설정할 수 있습니다.
기존에는 XML 파일을 수동으로 작성하거나, 별도의 라이브러리로 생성 스크립트를 만들어야 했다면, Next.js에서는 sitemap.ts 파일에 함수를 export하면 자동으로 sitemap.xml이 생성됩니다. 핵심 특징은 첫째, 동적 라우트를 포함한 모든 페이지를 자동으로 포함시킬 수 있고, 둘째, 데이터베이스에서 실시간으로 URL 목록을 가져올 수 있으며, 셋째, 빌드 타임이 아닌 런타임에 생성되어 항상 최신 상태를 유지한다는 점입니다.
이러한 특징들이 대규모 동적 사이트에서도 sitemap을 쉽게 관리할 수 있게 합니다.
코드 예제
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// 정적 페이지들
const staticRoutes = ['', '/about', '/contact'].map(route => ({
url: `https://mysite.com${route}`,
lastModified: new Date().toISOString(),
changeFrequency: 'monthly' as const,
priority: 0.8,
}))
// 동적 블로그 포스트들
const posts = await getAllPosts()
const postRoutes = posts.map(post => ({
url: `https://mysite.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.6,
}))
return [...staticRoutes, ...postRoutes]
}
설명
이것이 하는 일: sitemap.ts 파일에서 export default한 함수는 /sitemap.xml 경로로 접근할 때 실행되어, XML 형식의 sitemap을 반환합니다. 검색 엔진은 이 파일을 주기적으로 다운로드하여 새로운 페이지나 업데이트된 페이지를 발견합니다.
첫 번째로, 반환하는 배열의 각 객체는 하나의 URL을 나타냅니다. url은 절대 경로여야 하며, 프로토콜(https://)과 도메인을 반드시 포함해야 합니다.
lastModified는 ISO 8601 형식의 날짜로, 검색 엔진이 해당 페이지가 언제 변경되었는지 알 수 있게 합니다. 최근에 수정된 페이지일수록 더 자주 크롤링됩니다.
그 다음으로, changeFrequency는 'always', 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'never' 중 하나를 선택할 수 있습니다. 이는 검색 엔진에 대한 힌트일 뿐 강제력은 없지만, 크롤링 스케줄을 최적화하는 데 도움이 됩니다.
뉴스 사이트는 'daily'나 'hourly'를, 블로그는 'weekly'를, 회사 소개 페이지는 'monthly'를 사용하는 것이 적절합니다. 마지막으로, priority는 0.0에서 1.0 사이의 값으로, 사이트 내에서 페이지의 상대적 중요도를 나타냅니다.
홈페이지는 1.0, 주요 카테고리 페이지는 0.8, 개별 포스트는 0.6처럼 설정할 수 있습니다. 단, 이것도 힌트일 뿐이며, 검색 엔진은 자체 알고리즘으로 중요도를 판단합니다.
모든 페이지를 1.0으로 설정하면 의미가 없으므로 신중하게 차등을 두세요. 여러분이 이 코드를 사용하면 검색 엔진이 사이트의 모든 페이지를 빠르게 발견하고 인덱싱할 수 있습니다.
특히 새로운 콘텐츠가 추가되었을 때 즉시 검색 결과에 반영되도록 할 수 있습니다. 또한 크롤링 효율성이 개선되어, 크롤링 예산이 중요한 페이지에 집중되도록 할 수 있습니다.
실전 팁
💡 사이트가 크다면(5만 개 이상 URL) sitemap을 여러 개로 분할하고, sitemap index를 사용하세요. Next.js는 배열 대신 sitemap index를 반환하는 것도 지원합니다.
💡 구글 Search Console에서 sitemap.xml을 제출하면, 인덱싱 상태를 모니터링하고 오류를 확인할 수 있습니다.
💡 동적 데이터를 가져오는 경우, 에러 처리를 추가하여 데이터베이스 연결 실패 시에도 최소한의 sitemap이 생성되도록 하세요.
💡 로컬에서 http://localhost:3000/sitemap.xml로 접속하면 생성된 sitemap을 확인할 수 있습니다. 배포 전에 반드시 검증하세요.
💡 robots.txt 파일에 Sitemap: https://mysite.com/sitemap.xml를 추가하면 검색 엔진이 sitemap 위치를 자동으로 발견합니다.
8. 다국어 메타데이터 alternates
시작하며
여러분이 한국어와 영어로 서비스하는 글로벌 웹사이트를 운영한다면, 검색 엔진에게 "이 페이지는 한국어 버전이고, 영어 버전은 이 URL에 있어요"라고 알려줘야 합니다. 그렇지 않으면 구글이 중복 콘텐츠로 오해하거나, 한국 사용자에게 영어 페이지를 보여주는 문제가 발생합니다.
다국어 SEO는 매우 복잡한 분야이며, 잘못 설정하면 오히려 검색 순위가 떨어질 수 있습니다. 각 언어 버전이 서로 대체 가능한 콘텐츠임을 명시하고, 사용자의 언어에 맞는 버전으로 자동 리다이렉트되도록 해야 합니다.
바로 이럴 때 필요한 것이 alternates 메타데이터입니다. Next.js는 언어별 대체 URL을 간단하게 선언할 수 있는 방법을 제공합니다.
개요
간단히 말해서, alternates는 현재 페이지의 다른 언어 버전이나 대체 형식의 URL을 검색 엔진에 알려주는 메타데이터입니다. alternates의 가장 중요한 용도는 다국어 사이트에서 hreflang 태그를 생성하는 것입니다.
hreflang은 검색 엔진에게 "이 페이지는 한국어(ko) 사용자용이고, 영어 사용자를 위한 버전은 이 URL에 있어요"라고 알려줍니다. 이를 통해 구글은 사용자의 언어 설정에 맞는 페이지를 검색 결과에 표시합니다.
기존에는 <link rel="alternate" hreflang="ko" href="..." /> 태그를 모든 언어 버전에 수동으로 추가해야 했고, 언어가 추가될 때마다 모든 페이지를 수정해야 했다면, Next.js에서는 metadata.alternates.languages 객체만 정의하면 자동으로 처리됩니다. 핵심 특징은 첫째, 여러 언어와 지역 변형(en-US, en-GB 등)을 쉽게 선언할 수 있고, 둘째, canonical URL도 함께 설정하여 중복 콘텐츠 문제를 방지하며, 셋째, RSS 피드나 모바일 버전 같은 대체 형식도 지원한다는 점입니다.
이러한 특징들이 글로벌 SEO를 크게 단순화합니다.
코드 예제
// app/[lang]/blog/[slug]/page.tsx
import type { Metadata } from 'next'
export async function generateMetadata({ params }): Promise<Metadata> {
const { lang, slug } = params
const post = await getPost(slug, lang)
return {
title: post.title,
description: post.excerpt,
alternates: {
canonical: `https://mysite.com/${lang}/blog/${slug}`,
languages: {
'ko': `https://mysite.com/ko/blog/${slug}`,
'en': `https://mysite.com/en/blog/${slug}`,
'ja': `https://mysite.com/ja/blog/${slug}`,
'x-default': `https://mysite.com/en/blog/${slug}`,
},
},
}
}
설명
이것이 하는 일: alternates 설정은 <link rel="alternate" hreflang="..."> 태그들로 변환되어 HTML head에 삽입됩니다. 검색 엔진은 이 태그를 읽고, 사용자의 언어 설정에 맞는 페이지를 검색 결과에 표시합니다.
첫 번째로, languages 객체의 키는 언어 코드입니다. 'ko'는 한국어, 'en'은 영어, 'ja'는 일본어를 나타냅니다.
더 구체적으로 'en-US'(미국 영어), 'en-GB'(영국 영어), 'zh-CN'(중국 간체), 'zh-TW'(대만 번체) 같은 지역 변형도 지정할 수 있습니다. 이는 같은 언어라도 지역에 따라 다른 콘텐츠를 제공할 때 유용합니다.
그 다음으로, 'x-default'는 특별한 값으로, 지정된 언어에 해당하지 않는 사용자를 위한 기본 페이지를 나타냅니다. 일반적으로 영어 버전이나 언어 선택 페이지를 설정합니다.
예를 들어 아랍어 사용자가 접속했는데 아랍어 버전이 없다면, x-default로 지정된 페이지로 리다이렉트됩니다. 마지막으로, canonical URL은 중복 콘텐츠 문제를 해결하는 핵심입니다.
같은 콘텐츠가 여러 URL로 접근 가능한 경우(예: 쿼리 파라미터가 다른 경우), 대표 URL을 명시하면 검색 엔진이 이를 하나의 페이지로 간주합니다. 다국어 사이트에서는 각 언어 버전이 자기 자신을 canonical로 설정하여, 서로 다른 콘텐츠임을 명확히 합니다.
여러분이 이 코드를 사용하면 글로벌 사용자에게 최적화된 검색 결과를 제공할 수 있습니다. 한국 사용자는 한국어 페이지를, 일본 사용자는 일본어 페이지를 검색 결과에서 보게 되어, 클릭률과 사용자 만족도가 크게 향상됩니다.
또한 중복 콘텐츠 페널티를 피하고, 각 언어 버전이 독립적으로 검색 순위를 쌓아갈 수 있습니다.
실전 팁
💡 모든 언어 버전의 페이지에 동일한 hreflang 태그 세트를 추가해야 합니다. A 페이지에서 B를 가리키면, B 페이지에서도 A를 가리켜야 합니다.
💡 구글 Search Console의 International Targeting 보고서에서 hreflang 오류를 확인할 수 있습니다. 배포 후 반드시 검증하세요.
💡 URL 구조는 /ko/, /en/ 같은 서브디렉토리 방식이 가장 관리하기 쉽습니다. 서브도메인(ko.mysite.com)이나 별도 도메인(.co.kr)도 가능하지만 복잡합니다.
💡 기계 번역만으로 콘텐츠를 만들면 품질이 낮아 검색 순위가 떨어집니다. 중요한 페이지는 전문 번역가에게 의뢰하세요.
💡 언어별로 다른 CDN이나 서버를 사용한다면, 응답 속도도 SEO에 영향을 미치므로 각 지역에서 성능을 테스트하세요.
이 카드뉴스가 포함된 코스
댓글 (0)
함께 보면 좋은 카드 뉴스
마이크로서비스 배포 완벽 가이드
Kubernetes를 활용한 마이크로서비스 배포의 핵심 개념부터 실전 운영까지, 초급 개발자도 쉽게 따라할 수 있는 완벽 가이드입니다. 실무에서 바로 적용 가능한 배포 전략과 노하우를 담았습니다.
Application Load Balancer 완벽 가이드
AWS의 Application Load Balancer를 처음 배우는 개발자를 위한 실전 가이드입니다. ALB 생성부터 ECS 연동, 헬스 체크, HTTPS 설정까지 실무에 필요한 모든 내용을 다룹니다. 초급 개발자도 쉽게 따라할 수 있도록 단계별로 설명합니다.
고객 상담 AI 시스템 완벽 구축 가이드
AWS Bedrock Agent와 Knowledge Base를 활용하여 실시간 고객 상담 AI 시스템을 구축하는 방법을 단계별로 학습합니다. RAG 기반 지식 검색부터 Guardrails 안전 장치, 프론트엔드 연동까지 실무에 바로 적용 가능한 완전한 시스템을 만들어봅니다.
에러 처리와 폴백 완벽 가이드
AWS API 호출 시 발생하는 에러를 처리하고 폴백 전략을 구현하는 방법을 다룹니다. ThrottlingException부터 서킷 브레이커 패턴까지, 실전에서 바로 활용할 수 있는 안정적인 에러 처리 기법을 배웁니다.
AWS Bedrock 인용과 출처 표시 완벽 가이드
AWS Bedrock의 Citation 기능을 활용하여 AI 응답의 신뢰도를 높이는 방법을 배웁니다. 출처 추출부터 UI 표시, 검증까지 실무에서 바로 사용할 수 있는 완전한 가이드입니다.