Next.js SEO 100점 만들기 — 메타데이터, sitemap, robots.txt
PageSpeed SEO 점수 100점은 어렵지 않습니다. 몇 가지 설정만 빠짐없이 하면 됩니다. 문제는 놓치기 쉬운 항목이 있다는 겁니다. canonical URL, Open Graph 이미지, JSON-LD 같은 것들.
Next.js App Router 기준으로, SEO에 필요한 모든 설정을 정리했습니다.
1generateMetadata 패턴
Next.js App Router에서 메타데이터는 generateMetadata 함수로 설정합니다. 페이지마다 동적으로 생성할 수 있습니다.
// app/blog/[slug]/layout.tsx
import type { Metadata } from "next";
export async function generateMetadata({ params }): Promise<Metadata> {
const post = getPost(params.slug);
return {
title: post.metaTitle, // 60자 이내 권장
description: post.metaDescription, // 155자 이내 권장
keywords: post.tags,
authors: [{ name: post.author }],
// ⭐ canonical URL — 중복 인덱싱 방지
alternates: {
canonical: `/blog/${post.slug}`,
},
// Open Graph — SNS 공유 시 표시
openGraph: {
title: post.metaTitle,
description: post.metaDescription,
url: `https://example.com/blog/${post.slug}`,
type: "article",
images: [{
url: "https://example.com/opengraph-image",
width: 1200, height: 630,
alt: post.title,
}],
},
// Twitter Card
twitter: {
card: "summary_large_image",
title: post.metaTitle,
description: post.metaDescription,
},
};
}놓치기 쉬운 항목
- canonical URL: 없으면 www/non-www, http/https가 별도 페이지로 인식됩니다. 반드시 설정하세요.
- title 길이: 60자가 넘으면 검색 결과에서 잘립니다. metaTitle을 별도로 관리하는 게 좋습니다.
- description: 비어 있으면 Google이 자동 추출하는데, 원하는 내용이 아닐 수 있습니다.
2Open Graph 이미지 자동 생성
Next.js는 opengraph-image.tsx 파일을 만들면 OG 이미지를 자동으로 생성합니다. 매번 Figma에서 이미지를 만들 필요가 없습니다.
// app/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 function Image() {
return new ImageResponse(
(
<div style={{
display: "flex",
width: "100%",
height: "100%",
background: "linear-gradient(135deg, #1a1a2e, #16213e)",
color: "white",
alignItems: "center",
justifyContent: "center",
fontSize: 60,
fontWeight: 700,
}}>
Your Site Name
</div>
),
{ ...size }
);
}이 파일을 app/ 루트에 놓으면 전체 사이트의 기본 OG 이미지가 됩니다. 블로그 글마다 다른 이미지가 필요하면 app/blog/[slug]/opengraph-image.tsx에서 제목을 동적으로 렌더링할 수도 있습니다.
3sitemap.xml 동적 생성
sitemap.xml은 검색 엔진에게 "이 사이트에 어떤 페이지들이 있는지" 알려주는 파일입니다. Next.js에서 동적으로 생성할 수 있습니다.
// app/sitemap.xml/route.ts
import { getAllPosts } from "@/lib/blog";
export async function GET() {
const posts = getAllPosts();
const baseUrl = "https://example.com";
const staticPages = [
{ url: "/", lastmod: new Date().toISOString(), priority: 1.0 },
{ url: "/blog", lastmod: new Date().toISOString(), priority: 0.8 },
{ url: "/services/web", priority: 0.8 },
{ url: "/support", priority: 0.5 },
];
const blogPages = posts.map(post => ({
url: `/blog/${post.slug}`,
lastmod: post.date,
priority: 0.7,
}));
const pages = [...staticPages, ...blogPages];
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${pages.map(p => ` <url>
<loc>${baseUrl}${p.url}</loc>
${p.lastmod ? `<lastmod>${p.lastmod}</lastmod>` : ""}
<priority>${p.priority || 0.5}</priority>
</url>`).join("\n")}
</urlset>`;
return new Response(xml, {
headers: { "Content-Type": "application/xml" },
});
}sitemap 주의사항
- noindex 페이지 제외: 로그인, 관리자 페이지는 sitemap에 넣지 마세요.
- lastmod 정확히: 실제 수정 시점을 넣어야 합니다. 매번
new Date()를 넣으면 의미가 없습니다. - Google Search Console에 제출: sitemap.xml을 만들었으면 Search Console에서 제출해야 크롤러가 인식합니다.
4robots.txt 설정
robots.txt는 크롤러에게 "어디를 크롤링해도 되고, 어디를 하면 안 되는지" 알려줍니다.
// app/robots.txt/route.ts
export function GET() {
const content = `User-agent: *
Allow: /
Disallow: /admin
Disallow: /api/
Disallow: /auth/
Sitemap: https://example.com/sitemap.xml`;
return new Response(content, {
headers: { "Content-Type": "text/plain" },
});
}| 경로 | 설정 | 이유 |
|---|---|---|
| / | Allow | 공개 페이지는 크롤링 허용 |
| /admin | Disallow | 관리자 페이지 크롤링 차단 |
| /api/ | Disallow | API 엔드포인트 인덱싱 방지 |
| /auth/ | Disallow | 로그인/회원가입 인덱싱 불필요 |
5JSON-LD 구조화 데이터
JSON-LD는 검색 엔진에게 페이지의 콘텐츠를 구조화된 형태로 전달합니다. 블로그 글에 BlogPosting 스키마를 넣으면, 검색 결과에 작성일, 작성자, 설명이 리치 결과로 표시될 수 있습니다.
// components/blog/BlogJsonLd.tsx
export function BlogJsonLd({ post }) {
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.excerpt,
datePublished: post.date,
author: {
"@type": "Person",
name: post.author,
},
publisher: {
"@type": "Organization",
name: "Treeru",
url: "https://example.com",
},
mainEntityOfPage: {
"@type": "WebPage",
"@id": `https://example.com/blog/${post.slug}`,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
);
}JSON-LD 스키마 종류
| 페이지 유형 | 스키마 | 리치 결과 |
|---|---|---|
| 블로그 글 | BlogPosting | 작성일, 작성자 표시 |
| 회사 소개 | Organization | 로고, 연락처 패널 |
| FAQ | FAQPage | 펼침 FAQ 표시 |
| 제품/서비스 | Product/Service | 가격, 평점 표시 |
6IndexNow — 즉시 색인 요청
새 글을 발행하거나 기존 글을 수정하면, 검색 엔진이 크롤링할 때까지 기다려야 합니다. IndexNow를 사용하면 변경 사실을 즉시 알릴 수 있습니다.
// IndexNow API 호출
// Bing, Yandex, Naver(일부)에서 지원
const response = await fetch(
"https://api.indexnow.org/indexnow", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
host: "example.com",
key: "your-indexnow-key",
urlList: [
"https://example.com/blog/new-post",
"https://example.com/blog/updated-post",
],
}),
}
);
// 200 OK → 제출 성공
// 키 파일을 example.com/your-key.txt에 배치 필요IndexNow vs Google Ping
Google은 IndexNow를 지원하지 않습니다. 대신 Google Search Console의 URL 검사 도구에서 수동으로 색인 요청하거나, sitemap을 다시 제출하면 됩니다. 하지만 Bing은 IndexNow로 수 분 내에 인덱싱되므로 활용 가치가 있습니다.
SEO 100점 체크리스트
SEO 점수와 실제 검색 순위는 다릅니다. SEO 100점은 기술적 설정이 완벽하다는 의미이며, 검색 순위는 콘텐츠 품질, 백링크, 사용자 경험 등 다른 요소에 의해 결정됩니다.
댓글
(4개)로그인하면 댓글을 작성할 수 있습니다.
IndexNow로 발행 즉시 Bing에 알려주는 거, 실제로 해보니 몇 분 안에 인덱싱 되더라고요. 네이버도 IndexNow 지원하면 좋겠는데.
opengraph-image.tsx로 OG 이미지를 자동 생성하는 방법이 깔끔합니다. 매번 디자이너한테 요청했는데 이제 코드로 만들 수 있겠네요.
JSON-LD를 BlogPosting으로 넣으니 Google 검색 결과에 날짜랑 작성자가 표시되기 시작했습니다. 클릭률이 올라간 것 같아요.
관련 글
© 2026 TreeRU. All rights reserved.
본 콘텐츠의 저작권은 TreeRU에 있으며, 출처를 밝히지 않은 무단 전재 및 재배포를 금합니다. 인용 시 출처(treeru.com)를 반드시 명시해 주세요.