내 사이트, 구글에서 검색되게 만들기 — Next.js SEO 세팅법
사이트를 만들었는데, 구글에서 안 보인다
블로그를 열심히 만들고 포스트도 올렸는데, 구글에서 검색해보면 아무것도 안 나온다.
이게 당연한 거다. 사이트를 만든다고 구글이 알아서 찾아오지 않는다. 구글 크롤러가 사이트를 "발견"하고, "읽을 수 있고", "이해할 수 있어야" 검색 결과에 나타난다.
이걸 세팅해주는 작업이 SEO(Search Engine Optimization)다. 거창하게 들리지만 핵심은 딱 세 가지다.
- 크롤러가 들어올 수 있게 문 열어주기 (robots.txt)
- 사이트 지도 제출하기 (sitemap.xml)
- 각 페이지 정보 구조화하기 (메타태그, Open Graph, JSON-LD)
Next.js App Router 기준으로 이 세 가지를 직접 세팅한 과정을 공유한다.
사전 준비
- Next.js 13+ (App Router)
- 배포된 사이트 (도메인 필요)
- Google Search Console 계정
Step 1. robots.txt — 크롤러에게 문 열어주기
robots.txt는 크롤러에게 "이 사이트 어디까지 봐도 돼"를 알려주는 파일이다. 없으면 크롤러가 들어와도 혼란스러울 수 있다.
Next.js App Router에서는 파일 하나로 끝난다.
app/robots.ts 생성:
import type { MetadataRoute } from "next";
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
sitemap: "https://yourdomain.com/sitemap.xml",
};
}
빌드하면 자동으로 /robots.txt가 생성된다. yourdomain.com은 실제 도메인으로 바꿔주면 된다.
Step 2. sitemap.xml — 구글에 사이트 지도 제출
sitemap은 "내 사이트에는 이런 페이지들이 있어요"를 구글에 알려주는 지도다. 없으면 구글이 직접 크롤링해서 찾아야 하는데, 그러면 누락되는 페이지가 생긴다.
app/sitemap.ts 생성:
import type { MetadataRoute } from "next";
import { getAllPosts } from "@/lib/posts";
const BASE_URL = "https://yourdomain.com";
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getAllPosts();
const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${BASE_URL}/posts/${post.slug}`,
lastModified: post.date ? new Date(post.date) : new Date(),
changeFrequency: "monthly",
priority: 0.8,
}));
const staticEntries: MetadataRoute.Sitemap = [
{ url: BASE_URL, changeFrequency: "daily", priority: 1.0 },
{ url: `${BASE_URL}/posts`, changeFrequency: "daily", priority: 0.9 },
{ url: `${BASE_URL}/about`, changeFrequency: "monthly", priority: 0.5 },
];
return [...staticEntries, ...postEntries];
}
빌드 후 /sitemap.xml에서 확인할 수 있다.
다국어 사이트라면?
next-intl같은 i18n 라이브러리를 쓰고 있다면,/ko/posts/slug와/en/posts/slug를 모두 포함해야 한다. 그리고 fallback으로 보여주는 페이지(번역 없이 다른 언어 내용을 보여주는 경우)는 sitemap에서 제외하는 게 좋다. 중복 콘텐츠로 인식될 수 있기 때문이다.
Step 3. Open Graph — 공유했을 때 예쁘게 나오게
카카오톡, 슬랙, 트위터에 링크를 붙여넣으면 제목과 설명이 미리보기로 뜨는 걸 본 적 있을 거다. 그게 Open Graph 태그 덕분이다. 없으면 링크 텍스트만 달랑 나온다.
app/[locale]/layout.tsx (사이트 전역):
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
default: "내 블로그 이름",
template: `%s | 내 블로그 이름`, // 포스트 페이지에서 "포스트제목 | 블로그이름" 형식
},
description: "블로그 설명",
metadataBase: new URL("https://yourdomain.com"),
openGraph: {
type: "website",
siteName: "내 블로그 이름",
title: "내 블로그 이름",
description: "블로그 설명",
},
twitter: {
card: "summary_large_image",
},
};
}
app/[locale]/posts/[slug]/page.tsx (포스트 개별 페이지):
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug, locale } = await params;
const post = await getPost(slug, locale);
if (!post) return {};
return {
title: post.title,
description: post.description,
alternates: {
canonical: `https://yourdomain.com/${locale}/posts/${slug}`,
},
openGraph: {
type: "article", // 블로그 글임을 명시
title: post.title,
description: post.description,
publishedTime: post.date ? new Date(post.date).toISOString() : undefined,
tags: post.tags,
},
};
}
canonical은 "이 페이지의 정식 URL은 여기야"라고 알려주는 것이다. 다국어 사이트에서 같은 내용이 여러 URL에 있을 때 중복 콘텐츠 패널티를 막아준다.
Step 4. JSON-LD — 구글이 블로그 글임을 확실히 인식
JSON-LD는 페이지에 숨겨진 구조화 데이터다. 구글이 이걸 읽고 "아, 이건 블로그 포스트구나"라고 정확히 이해한다. 잘 되면 검색 결과에 리치카드(작성일, 태그 등)가 표시되기도 한다.
포스트 페이지 컴포넌트 안에 추가:
const jsonLd = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.date ? new Date(post.date).toISOString() : undefined,
url: `https://yourdomain.com/posts/${slug}`,
author: {
"@type": "Person",
name: "블로그 작성자 이름",
url: "https://yourdomain.com",
},
keywords: post.tags?.join(", "),
};
return (
<article>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* 나머지 포스트 내용 */}
</article>
);
Step 5. Google Search Console 등록 및 sitemap 제출
여기까지 했으면 이제 구글한테 직접 알려줘야 한다.
1. Search Console 접속 및 속성 추가
search.google.com/search-console → 속성 추가 → URL 접두어 → 도메인 입력
2. 소유권 확인
HTML 태그 방법 선택 → 아래 같은 코드 발급:
<meta name="google-site-verification" content="여기에값이들어옴" />
Next.js에서는 generateMetadata에 한 줄 추가:
verification: {
google: "여기에값이들어옴",
},
배포 후 Search Console에서 확인 클릭.
3. sitemap 제출
좌측 메뉴 → Sitemaps → sitemap.xml 입력 → 제출
트러블슈팅
sitemap.xml에 접속했을 때 빈 화면이 나온다
- 빌드가 완료됐는지 확인
getAllPosts()같은 데이터 fetch 함수에서 에러가 나는지 확인- 프로덕션에서만 동작하는 환경변수가 있다면 Vercel 환경변수 세팅 확인
Search Console에서 "URL을 가져올 수 없음" 오류
- robots.txt에서 해당 URL을 차단하고 있지 않은지 확인
- 사이트가 실제로 배포된 상태인지 확인
- Vercel 배포 직후라면 1~2분 대기 후 재시도
메타태그가 적용됐는지 확인하는 법
브라우저에서 페이지 소스 보기(Cmd+U)로 <meta property="og:title" 검색하거나, opengraph.xyz에서 URL 입력하면 미리보기로 확인 가능하다.
정리 — 핵심 흐름 한눈에
1. app/robots.ts → 크롤러 허용 + sitemap 위치 안내
2. app/sitemap.ts → 모든 페이지 URL 목록 생성
3. layout.tsx → 사이트 전역 OG/Twitter 메타태그
4. posts/[slug]/page → 포스트별 canonical + OG article + JSON-LD
5. Search Console → 소유권 인증 → sitemap 제출
한 번 세팅해두면 이후 포스트를 올릴 때마다 sitemap이 자동으로 업데이트된다. 검색 노출은 제출 후 1~2주 정도 걸리는 게 일반적이다. 기다리는 동안 포스트를 꾸준히 올리는 게 가장 좋은 SEO 전략이다.
backtodev
40대 PM, 다시 개발자로 돌아갑니다. 실패하고 배우며 성장하는 기록.