Next.js (App Router) 블로그 SEO 최적화 실전 가이드
직접 블로그를 만들고 구글과 네이버 검색에 노출시키기 위해 적용했던 Next.js App Router 기반의 SEO 최적화 과정과 삽질 기록입니다.
블로그를 뚝딱 만들고 Vercel에 배포했을 때만 해도 다 끝난 줄 알았습니다. 하지만 며칠이 지나도 구글이나 네이버에서 제 글을 찾을 수가 없었죠.
검색 엔진이 알아서 내 사이트를 찾아올 거라는 건 사실 꽤나 순진한 생각이었습니다. 검색 봇이 문을 두드리기 쉽게 길을 닦아주는 작업, 바로 SEO(검색엔진 최적화)가 필요했습니다.
이번 글에서는 Next.js App Router 환경에서 블로그를 처음 만들고, 어떻게든 구글과 네이버 검색에 노출시켜보려고 적용했던 방법들과 그 과정에서 겪었던 시행착오를 정리해 보려고 합니다.
가장 먼저, 동적 메타데이터(Metadata) 챙기기
App Router로 넘어오면서 가장 체감이 컸던 변화 중 하나는 generateMetadata 함수의 도입이었습니다. 예전 Pages Router 시절에는 next/head 컴포넌트 안에 태그를 일일이 우겨넣느라 꽤 피곤했는데, 이제는 페이지 상단에서 훨씬 직관적으로 데이터를 뽑아낼 수 있게 되었습니다.
제가 블로그 상세 페이지(/app/blog/[slug]/page.tsx)에 메타데이터를 적용한 뼈대는 대략 이렇습니다.
import type { Metadata } from "next"
import { getPostBySlug } from "@/lib/posts"
import { absoluteUrl, siteConfig } from "@/lib/site"
export async function generateMetadata({ params }): Promise<Metadata> {
const { slug } = await params
const post = await getPostBySlug(slug)
if (!post) {
return { title: "Post not found" }
}
const canonical = absoluteUrl(`/blog/${post.slug}`)
return {
title: post.title,
description: post.description,
authors: [{ name: siteConfig.author }],
alternates: {
canonical
},
openGraph: {
title: post.title,
description: post.description,
type: "article",
url: canonical,
// ... 그 외 필요한 사이트 설정들
}
}
}이 코드를 짜면서 제일 크게 신경 썼던 부분은 canonical 태그를 명시하는 것이었습니다. 나중에 UTM이나 각종 쿼리 파라미터가 덕지덕지 붙더라도 검색 엔진이 "아, 이 글의 원본 주소는 여기구나"라고 명확히 인식하게 만들어 줘서 중복 문서로 불이익을 당하는 일을 막아줍니다.
길잡이 역할, sitemap.xml과 robots.txt
메타데이터가 각 글의 '명함'이라면, 사이트맵과 robots.txt는 우리 블로그 전체의 '조감도'와 '초대장' 역할을 합니다.
Next.js에서는 굉장히 훌륭한 기능을 제공하는데요. 최상단 app 폴더 아래에 sitemap.ts와 robots.ts 파일을 만들어 두면 프레임워크가 알아서 서버 사이드에서 최신 데이터를 XML과 텍스트 형태의 파일로 찍어내 줍니다.
특히 블로그는 새 글이 지속적으로 추가되기 때문에 동적으로 갱신되는 사이트맵이 필수입니다.
import type { MetadataRoute } from "next"
import { getAllPosts } from "@/lib/posts"
import { absoluteUrl } from "@/lib/site"
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
// 포스트 데이터를 긁어옵니다
const posts = await getAllPosts()
const postRoutes = posts.map((post) => ({
url: absoluteUrl(`/blog/${post.slug}`),
lastModified: new Date(post.updatedAt ?? post.date),
changeFrequency: "monthly" as const,
priority: 0.8,
}))
// 블로그 메인 같은 정적 라우트와 합쳐서 리턴해주면 끝입니다
return [...baseRoutes, ...postRoutes]
}이렇게 세팅을 마치고 https://내블로그/sitemap.xml로 접속했을 때, 제가 쓴 글들의 주소가 XML 형태로 예쁘게 나열된 걸 보면서 묘한 쾌감을 느꼈던 기억이 납니다.
검색 엔진에 조금 더 친절하게, 구조화된 데이터(JSON-LD)
이 부분은 처음 블로그를 만들 때는 "굳이 개인 블로그에 이런 것까지 해야 할까?" 싶어 건너뛰려 했습니다. 하지만 구글 검색 결과에서 가끔 기사나 레시피가 예쁜 포맷으로 깔끔하게 정리되어 나오는 걸 보고 욕심이 생겨 적용해 봤습니다.
Next.js 공식 문서에서도 이 JSON-LD 방식을 강력하게 권장하고 있습니다. 페이지 본문 어딘가에 <script type="application/ld+json"> 태그를 삽입해서 봇들이 씹고 뜯고 맛보기 좋게 텍스트를 정제해 두는 식입니다.
const structuredData = {
"@context": "https://schema.org",
"@type": "BlogPosting",
headline: post.title,
description: post.description,
datePublished: post.date,
author: {
"@type": "Person",
name: "작성자 이름" // 본인 이름을 넣어줍니다
}
}
// return 블록 어딘가에 살며시 넣어줍니다
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(structuredData) }}
/>이 구조화된 데이터를 꼼꼼히 입혀 두면, 구글이 내 페이지를 단순한 잡록이 아니라 명확한 '블로그 아티클'로 인식하고 검색 결과에서 조금 더 눈에 띄게 대우해 줄 확률이 높아집니다.
코딩이 다가 아니었다: 구글과 네이버에 핑(Ping) 보내기
앞의 모든 코드를 완벽하게 짰다 해도 진짜 작업은 지금부터였습니다. 문을 고쳐 달았다고 손님이 알아서 들어오는 건 아니기 때문에 가장 중요한 봇들을 직접 초대해야 했습니다.
- Google Search Console: 가장 먼저 도메인을 인증했습니다. 그리고 심사숙고해서 생성해 둔
sitemap.xml경로를 보란 듯이 제출했습니다. - Naver Search Advisor: 네이버 서치어드바이저 역시 비슷하게 사이트 소유 확인을 거친 뒤 RSS와 사이트맵 제출을 마쳤습니다.
그런데 한 가지 당황스러웠던 점은, 사이트맵을 던져줬다고 바로 다음 날 제 글이 검색되는 건 아니라는 거였습니다. 구글은 비교적 금방 긁어갔지만, 네이버의 경우에는 며칠 여유를 갖고 기다려야 했습니다. 정 안 되겠다 싶을 땐 마음을 비우고 '웹 페이지 수집 요청' 메뉴에 들어가 제 글 주소를 한 땀 한 땀 정성스레 입력하는 고전적인 방법도 병행했습니다.
마무리하며 느낀 점
이 과정을 거치면서 검색 엔진 최적화는 '한 번 스크립트 툭 짜고 끝'인 단발성 작업이 아니라는 걸 뼈저리게 느꼈습니다.
처음 설정하고 한 달 정도는 색인 생성 여부를 서치콘솔에서 매일 들여다보고, 알 수 없는 이유로 누락된(발견됨 - 현재 색인 생성되지 않음) 페이지가 있으면 수동으로 색인 요청 버튼을 누르며 꾸준하게 관심을 가져야만 했습니다.
SEO의 본질은 결국 "내가 정성껏 짠 코드가 사람뿐만 아니라 기계(봇)가 이해하기에도 매끄러운 형태인가?"를 고민하는 일련의 과정이었던 것 같습니다. 그렇게 노력을 들인 끝에 며칠 전 구글에 제 글 키워드를 쳤을 때 첫 페이지 상단에 걸려 있는 걸 보면서, 꽤 오랜만에 순수한 코딩의 기쁨을 느꼈습니다.
블로그를 배포하고 검색 유입이 없어서 저처럼 막막하셨던 분들께, 이 삽질 기록이 조금이나마 다음 스텝을 밟기 위한 단서가 되길 바랍니다.