검색 엔진 최적화(SEO)는 단순히 정보를 나열하는 것을 넘어, 검색 로봇이 내 콘텐츠를 얼마나 정확하게 이해하고 사용자에게 전달하느냐를 결정하는 핵심 요소입니다.
Next.js 14의 App Router 환경에서 제공하는 Metadata API와 Dynamic Routes를 활용해, 정적 정보부터 동적 포스트까지 검색 엔진에 완벽히 대응하는 SEO 전략을 구축한 과정을 공유합니다.
1. 기본 메타데이터 및 인증 설정: 블로그의 정체성 정의
블로그의 전체적인 정체성을 나타내는 layout.tsx 혹은 page.tsx의 정적 메타데이터는 사이트의 신뢰도를 결정합니다.
핵심 메커니즘:
Metadata인터페이스를 활용해 OpenGraph(OG)와 Twitter Card 정보를 설정합니다. 이는 소셜 미디어 공유 시 풍부한 미리보기를 제공합니다.검증(Verification): 구글 서치 콘솔 등 외부 도구와의 연동을 위해
verification필드를 사용하여 소유권을 증명합니다.
export const metadata: Metadata = {
title: '동그라미 블로그',
description: '마크다운 기반 개발 콘텐츠 공유 블로그입니다.',
robots: 'index, follow',
metadataBase: new URL('https://blog-nu-dun-70.vercel.app'),
// ... OpenGraph, Twitter 설정
};OpenGraph, Twitter 등록을 해서 카카오톡에 링크를 공유하면 아래와 같이 표시가 됩니다.
2. 동적 포스트 SEO: generateMetadata를 통한 자동화
동적으로 생성되는 상세 포스트 페이지는 각 콘텐츠의 내용에 맞는 메타데이터가 필요합니다. Next.js의 generateMetadata 함수를 사용하면 해당 포스트의 데이터를 페치(Fetch)하여 실시간으로 메타 정보를 구성할 수 있습니다.
🛠️ 효율적인 데이터 요약 전략
HTML 태그 제거: 마크다운이나 위지윅(WYSIWYG) 데이터에는 HTML 태그가 포함되어 있습니다. 정규식을 통해 태그를 제거한 순수 텍스트(Plain Text)를 추출합니다.
설명글(Description) 최적화: 본문 앞부분을 약 150자 정도로 잘라 검색 결과 하단에 노출될 설명을 자동으로 생성합니다.
export async function generateMetadata({ params }: { params: { postId: string } }) {
const res = await requestHttp.get<PostDataResponse>(`${INTERNAL_URL_IN_NODE.POSTS}/${params.postId}`);
const post = res.data;
const plainText = post.content.replace(/<[^>]+>/g, '').trim();
const description = plainText.slice(0, 150);
return {
title: post.title,
description,
openGraph: { type: 'article', title: post.title, description }, // type을 article로 명시
};
}3. 검색 엔진의 이정표: robots.txt와 sitemap.xml 자동 생성
검색 로봇이 사이트의 구조를 빠르게 파악하도록 돕는 robots.ts와 sitemap.ts를 구현했습니다.
🛠️ 시스템 최적화
robots.ts: 관리자 API(
/api/)나 인증 페이지(/login/) 등 불필요한 크롤링을 방지하여 크롤링 예산(Crawl Budget)을 아낍니다.sitemap.ts: 백엔드에서 포스트 목록을 페치하여 전체 경로를 동적으로 생성합니다. 포스트별
updatedAt날짜를lastModified에 연결하여 최신 정보 업데이트 주기를 명시합니다.
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const res = await requestHttp.get<PostsSitemapDataResonse[]>(INTERNAL_URL_IN_NODE.POSTS_SITEMAP);
const posts = res.data ?? [];
const postUrls = posts.map(post => ({
url: `${BASE_URL}/post/${post.id}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [ { url: BASE_URL, priority: 1 }, ...postUrls ];
}사이트맵은 게시글이 늘어나 서버 부하가 발생할 여지가 있기 때문에, 블로그 서버 측에서 사이트맵 전용 엔드포인트를 추가해서 최소한의 응답값 리소스를 응답하게 했습니다.
async findAllForSitemap() {
return this.prisma.post.findMany({
where: { published: true },
select: {
id: true,
updatedAt: true,
},
orderBy: { createdAt: Order.desc },
});
}4. 기술적 디테일: 데이터 무결성과 크롤링 효율
Next.js의 특성을 활용해 성능과 SEO를 동시에 잡았습니다.
조건부 리디렉션 처리: 캐시 이슈로 인해 포스트가 존재하지 않을 경우
404리디렉션을 즉시 수행하여 검색 엔진이 유효하지 않은 페이지를 인덱싱하지 않도록 관리합니다.Route Handler 활용: 사이트맵 데이터를 서빙하기 위한 전용 GET 핸들러를 구축하여 외부에서도 쉽게 접근할 수 있는 구조를 마련했습니다.
5. 구글서치콘솔 등록하기
코드를 배포했다면, 이제 구글 서치콘솔에 내 사이트를 등록해야 합니다.
속성 유형 선택: 간단한 1개의 주소만 분석을 원했기 때문에 URL 접두어 방식을 선택했습니다.
소유권 인증: Next.js 코드 내
verification설정에 구글에서 발급받은google-site-verification코드를 삽입하여 배포함으로써, 내가 이 사이트의 관리자임을 증명합니다.
export const metadata: Metadata = {
verification: {
google: '서치콘솔 인증 키', // 서치콘솔 인증 키
},
};소유권 확인이 완료되었다면, 구글봇이 내 사이트의 구조를 한눈에 파악할 수 있도록 앞서 만든 sitemap.xml의 경로를 알려줘야 합니다.
6. 마치며
SEO는 한 번의 설정으로 끝나지 않는 지속적인 개선 과정입니다. 이번 설정을 통해 블로그의 모든 포스트가 검색 결과에 효과적으로 노출될 수 있는 견고한 토대를 마련했습니다. 앞으로는 구글 서치 콘솔의 색인 보고서를 모니터링하며, 검색 노출 빈도가 높은 키워드를 메타데이터에 더 정교하게 반영해 나갈 예정입니다.