logo

DowanKim

동적 라우트 페이지 사이트맵에 포함시키는 방법

2026년 1월 16일

일상

image.png

Google Search Console에서 색인 상태를 확인했을 때:

  • 색인 생성된 페이지: 1개
  • 색인 생성되지 않은 페이지: 여러 개
  • 리디렉션이 포함된 페이지: 2개
  • 사용자가 선택한 표준이 없는 중복 페이지: 1개

와 같은 문제가 있었습니다.

원인 분석:

  1. 사이트맵에 동적 라우트가 포함되지 않음
  2. 일부 페이지에 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)

설정 방법:

  1. 동적 라우트: generateMetadata 함수 사용
  2. 정적 페이지: metadata export 사용

공통 규칙:

  • 절대 경로 사용 (https://dowankim.site/...)
  • URL 인코딩 적용 (encodeURIComponent)
  • 실제 접근 가능한 URL과 일치

요약

동적 라우트 사이트맵 추가

  1. async function sitemap() 사용
  2. 데이터베이스에서 데이터 가져오기
  3. map()으로 동적 URL 생성
  4. encodeURIComponent()로 URL 인코딩

배포 없이 새 글이 포함되는 이유

  1. 사이트맵은 요청 시마다 동적으로 생성됨
  2. Firebase에서 최신 데이터를 가져옴
  3. 빌드 시점이 아니라 요청 시점에 생성됨
  4. 배포 없이도 새 글이 즉시 사이트맵에 포함됨

구글이 동적 라우트를 읽는 방식

  1. 사이트맵에서 발견 (가장 빠름) ← 권장
  2. 링크를 따라 발견 (느림)
  3. 직접 크롤링 (매우 느림)

Canonical URL 필요성

  • 모든 페이지에 설정 권장
  • 중복 콘텐츠 문제 방지
  • 검색 결과 품질 향상
  • 동적 라우트는 generateMetadata 사용

새 글 빠른 색인 팁

  1. Google Search Console의 "URL 검사" 도구 사용
  2. "색인 생성 요청" 클릭
  3. 보통 몇 분~몇 시간 내에 색인 생성

Seo 를 제대로 챙기고 있는 줄 알았는데, 구글 서치 콘솔에 들어가보니 그렇지 않았던 것을 보고 그 이유를 파악하면서 공부한 과정입니다.

Canonical URL을 포함한 MetaData를 매 페이지 마다 코드작성하는 건 귀찮을 수 있다고 생각합니다. 이럴 때 Cursor 같은 AI의 도움을 빌려 구현한 것의 요약을 담는 메타데이터를 만들고 본인이 입맛에 맞게 수정하면, 좋지 않을까 생각도 듭니다.

많은 분들이 SEO를 챙기고자 Next 같은 툴을 사용할 것 같은데, Google Search Console에서, 내 사이트가 제대로 인식되고 있는지, 틈틈이 확인해 보시길 바랍니다 :)