동적 라우트 페이지 사이트맵에 포함시키는 방법
2026년 1월 16일

Google Search Console에서 색인 상태를 확인했을 때:
- 색인 생성된 페이지: 1개
- 색인 생성되지 않은 페이지: 여러 개
- 리디렉션이 포함된 페이지: 2개
- 사용자가 선택한 표준이 없는 중복 페이지: 1개
와 같은 문제가 있었습니다.
원인 분석:
- 사이트맵에 동적 라우트가 포함되지 않음
- 일부 페이지에 Canonical URL이 없음
이런 문제가 있었고, 또 기존 아이디어에서 있었던 url이 이후 구현에서 삭제하게되면서 남아있던 문제-> 이게 리디렉션이 되는 이슈가 있었지 않나 생각합니다.
1. 동적 라우트를 사이트맵에 추가하는 방법
문제: 정적 사이트맵의 한계
초기 사이트맵 (정적):
export default function sitemap(): MetadataRoute.Sitemap { return [ { url: `${baseUrl}/`, ... }, { url: `${baseUrl}/blog`, ... }, // 동적 라우트는 어떻게 추가하지 // /blog/[tag] - 태그가 몇 개인지 모름 // /blog/[tag]/[slug] - 글이 몇 개인지 모름 ] }
문제점:
- 동적 라우트는 빌드 시점에 개수를 알 수 없음
- Firebase에서 가져와야 하는 데이터
- 정적 사이트맵으로는 불가능
해결: Async 사이트맵으로 동적 생성
Next.js의 async sitemap 지원:
export default async function sitemap(): Promise<MetadataRoute.Sitemap> { // Firebase에서 데이터 가져오기 const tags = await getAllTags() const posts = await getPublishedPosts() // 동적으로 사이트맵 생성 // ... }
실제 구현 코드
// app/sitemap.ts export default async function sitemap(): Promise<MetadataRoute.Sitemap> { const now = new Date() // 기본 정적 페이지 const staticPages: MetadataRoute.Sitemap = [ { url: `${baseUrl}/`, ... }, { url: `${baseUrl}/blog`, ... }, { url: `${baseUrl}/daily`, ... }, ] try { // 태그 페이지들 (/blog/[tag]) const tags = await getAllTags() const tagPages: MetadataRoute.Sitemap = tags .filter(tag => tag !== '일상') .map(tag => ({ url: `${baseUrl}/blog/${encodeURIComponent(tag)}`, lastModified: now, changeFrequency: 'weekly', priority: 0.7, })) // 블로그 글 페이지들 (/blog/[tag]/[slug]) const posts = await getPublishedPosts() const postPages: MetadataRoute.Sitemap = posts.map(post => { const tag = post.tags && post.tags.length > 0 ? post.tags[0] : '' const slug = post.slug || post.id || '' return { url: `${baseUrl}/blog/${encodeURIComponent(tag)}/${encodeURIComponent(slug)}`, lastModified: updatedAt, changeFrequency: 'monthly', priority: 0.6, } }) return [...staticPages, ...tagPages, ...postPages] } catch (error) { return staticPages } }
1. Async 함수로 변경
// Before export default function sitemap(): MetadataRoute.Sitemap // After export default async function sitemap(): Promise<MetadataRoute.Sitemap>
2. 데이터베이스에서 데이터 가져오기
const tags = await getAllTags() // 모든 태그 가져오기 const posts = await getPublishedPosts() // 모든 게시글 가져오기
3. 동적으로 URL 생성
tags.map(tag => ({ url: `${baseUrl}/blog/${encodeURIComponent(tag)}`, // ... }))
4. URL 인코딩 필수
- 한글 태그나 특수문자가 포함될 수 있음
encodeURIComponent()로 안전하게 인코딩
2. 배포 없이 새 글이 사이트맵에 포함되는 이유
사이트맵은 요청 시마다 실시간으로 생성됩니다
새 글 작성 시 일어나는 일:
1. 어드민 페이지에서 글 작성
↓
2. Firebase에 저장 (published: true)
↓
3. 즉시 페이지 접근 가능
/blog/태그/슬러그 ← 바로 접근 가능
배포 없이 바로 접근 가능한 이유:
- Next.js의 동적 라우트(
[tag],[slug])는 빌드 시점에 고정되지 않음 - 요청 시마다 Firebase에서 데이터를 가져와 렌더링
여기까지는 저희가 이미 알고 있는 내용입니다. 동적 라우트의 배경 지식이지요..
사이트맵은 언제 생성되나?
async function sitemap()는 요청 시마다 실행됨await getPublishedPosts()로 Firebase에서 최신 데이터를 가져옴- 빌드 시점이 아니라 요청 시점에 생성됨
구글이 사이트맵을 요청할 때:
1. 구글이 /sitemap.xml 요청
↓
2. Next.js가 sitemap() 함수 실행
↓
3. Firebase에서 최신 posts 가져오기
(방금 작성한 글도 포함!)
↓
4. 사이트맵 XML 생성 (새 글 URL 포함)
↓
5. 구글이 새 URL 발견
- 배포가 필요 없음
- 새 글이 Firebase에 저장되면 즉시 사이트맵에 포함됨
- 구글이 사이트맵을 크롤링하면 자동으로 발견
정적 사이트 vs 동적 사이트
| 구분 | 정적 사이트 (빌드 시 생성) | 동적 사이트 (요청 시 생성) |
|---|---|---|
| 사이트맵 | 빌드 시 생성 | 요청 시마다 생성 |
| 새 글 추가 | 재배포 필요 | 즉시 반영 |
| 구글 발견 | 느림 (재배포 후) | 빠름 (사이트맵 크롤링 시) |
현재 프로젝트는:
- 동적 생성 방식
- 배포 없이도 새 글이 사이트맵에 포함됨
- 구글이 사이트맵을 크롤링하면 자동으로 발견
3. 구글이 동적 라우트 페이지를 읽는 방식
구글 크롤러의 페이지 발견 방법
3가지 방법 (우선순위 순):
1. 사이트맵에서 발견 (가장 빠름)
사이트맵 제출
↓
구글이 사이트맵 다운로드
↓
사이트맵에 있는 모든 URL 크롤링
↓
색인 생성
장점:
- 가장 빠른 발견
- 모든 페이지를 빠르게 색인
- 우선순위와 업데이트 빈도 정보 활용
2. 다른 페이지의 링크를 따라 발견 (느림)
홈페이지 크롤링
↓
홈페이지의 링크 발견 (/blog)
↓
/blog 페이지 크롤링
↓
/blog 페이지의 링크 발견 (/blog/태그)
↓
태그 페이지 크롤링
...
문제점:
- 깊이 있는 페이지는 발견이 늦음
- 링크가 없으면 발견 불가
- 시간이 오래 걸림
3. 직접 크롤링 (매우 느림)
구글이 무작위로 URL 시도
↓
존재하는 페이지 발견
↓
색인 생성
문제점:
- 매우 느림
- 모든 페이지를 발견하지 못할 수 있음
- 비효율적
구글이 사이트맵을 얼마나 자주 읽는데?
문제점:
- 구글은 사이트맵을 매일 읽지 않음
- 보통 며칠~몇 주 간격으로 크롤링
- 새 글이 바로 사이트맵에 나타나지 않을 수 있음
해결 방법:
방법 1: Google Search Console에서 색인 생성 요청
1. Google Search Console 접속
↓
2. "URL 검사" 도구 사용
↓
3. 새 글 URL 입력
↓
4. "색인 생성 요청" 클릭
↓
5. 구글이 즉시 크롤링 (보통 몇 분~몇 시간)
방법 2: 기다리기
- 구글이 주기적으로 사이트맵을 크롤링
- 보통 며칠 내에 새 글 발견
내가 작성한 글이 바로바로 떴으면 좋겠으면 수동으로 색인생성 요청을 하면 되고, 사실 그냥 냅둬도 좀 기다리면 알아서 나중에 올라올 것입니다.
4. Canonical URL을 페이지마다 모두 만들어야 하는가?
네, 모든 페이지에 설정하는 것이 좋습니다
Canonical URL이란?
- 페이지의 원본 URL을 명시하는 메타 태그
- 중복 콘텐츠 문제 해결
- 구글이 어떤 URL을 색인해야 할지 알려줌
HTML 형태:
<link rel="canonical" href="https://dowankim.site/blog/태그/슬러그" />
왜 모든 페이지에 필요한가?
1. 중복 콘텐츠 문제 방지
문제 상황:
같은 콘텐츠가 여러 URL에 존재할 수 있음:
- https://dowankim.site/blog/태그/슬러그
- https://dowankim.site/blog/태그/슬러그?ref=share
- https://dowankim.site/blog/태그/슬러그?utm_source=twitter
해결:
alternates: { canonical: 'https://dowankim.site/blog/태그/슬러그' }
→ 구글이 원본 URL을 인식
2. 동적 라우트의 URL 변형 문제
문제:
- 한글 태그는 URL 인코딩 방식에 따라 다를 수 있음
- 구글이 다른 URL로 인식할 수 있음
해결:
- Canonical URL로 원본 명시
- 구글이 정확한 URL 인식
3. 검색 결과 품질 향상
Canonical이 있을 때:
- 구글이 원본 URL에 집중
- 검색 결과에 올바른 URL 표시
- SEO 점수 향상
Canonical이 없을 때:
- 구글이 혼란스러워함
- 잘못된 URL이 색인될 수 있음
- SEO 점수 하락
실제 구현 예시
1. 블로그 글 페이지 (동적 메타데이터)
// app/blog/[tag]/[slug]/page.tsx export async function generateMetadata({ params }: PageProps): Promise<Metadata> { const decodedSlug = decodeURIComponent(params.slug) const decodedTag = decodeURIComponent(params.tag) const post = await getPostBySlug(decodedSlug) const canonicalUrl = `${siteUrl}/blog/${encodeURIComponent(decodedTag)}/${encodeURIComponent( decodedSlug )}` return { title: post.title, description, alternates: { canonical: canonicalUrl, // Canonical URL 설정 }, // ... } }
2. 태그 페이지 (동적 메타데이터)
// app/blog/[tag]/page.tsx export async function generateMetadata({ params }: PageProps): Promise<Metadata> { const decodedTag = decodeURIComponent(params.tag) const canonicalUrl = `${siteUrl}/blog/${encodeURIComponent(decodedTag)}` return { title: decodedTag, alternates: { canonical: canonicalUrl, // Canonical URL 설정 }, // ... } }
3. 정적 페이지 (정적 메타데이터)
// app/daily/page.tsx export const metadata: Metadata = { title: 'Daily', alternates: { canonical: `${siteUrl}/daily`, // Canonical URL 설정 }, // ... }
Canonical URL 설정 가이드
설정이 필요한 페이지:
- 모든 페이지 (홈, 블로그, Daily 등)
- 동적 라우트 페이지 (
/blog/[tag],/blog/[tag]/[slug]) - 정적 페이지 (
/blog,/daily)
설정 방법:
- 동적 라우트:
generateMetadata함수 사용 - 정적 페이지:
metadataexport 사용
공통 규칙:
- 절대 경로 사용 (
https://dowankim.site/...) - URL 인코딩 적용 (
encodeURIComponent) - 실제 접근 가능한 URL과 일치
요약
동적 라우트 사이트맵 추가
async function sitemap()사용- 데이터베이스에서 데이터 가져오기
map()으로 동적 URL 생성encodeURIComponent()로 URL 인코딩
배포 없이 새 글이 포함되는 이유
- 사이트맵은 요청 시마다 동적으로 생성됨
- Firebase에서 최신 데이터를 가져옴
- 빌드 시점이 아니라 요청 시점에 생성됨
- 배포 없이도 새 글이 즉시 사이트맵에 포함됨
구글이 동적 라우트를 읽는 방식
- 사이트맵에서 발견 (가장 빠름) ← 권장
- 링크를 따라 발견 (느림)
- 직접 크롤링 (매우 느림)
Canonical URL 필요성
- 모든 페이지에 설정 권장
- 중복 콘텐츠 문제 방지
- 검색 결과 품질 향상
- 동적 라우트는
generateMetadata사용
새 글 빠른 색인 팁
- Google Search Console의 "URL 검사" 도구 사용
- "색인 생성 요청" 클릭
- 보통 몇 분~몇 시간 내에 색인 생성
Seo 를 제대로 챙기고 있는 줄 알았는데, 구글 서치 콘솔에 들어가보니 그렇지 않았던 것을 보고 그 이유를 파악하면서 공부한 과정입니다.
Canonical URL을 포함한 MetaData를 매 페이지 마다 코드작성하는 건 귀찮을 수 있다고 생각합니다. 이럴 때 Cursor 같은 AI의 도움을 빌려 구현한 것의 요약을 담는 메타데이터를 만들고 본인이 입맛에 맞게 수정하면, 좋지 않을까 생각도 듭니다.
많은 분들이 SEO를 챙기고자 Next 같은 툴을 사용할 것 같은데, Google Search Console에서, 내 사이트가 제대로 인식되고 있는지, 틈틈이 확인해 보시길 바랍니다 :)