sitemap.xml의 lastmod를 new Date()로 넣지 말자
Next.js의 `app/sitemap.ts`를 오랜만에 다시 봤는데, 코드가 대략 이런 모양이었다.
export default function sitemap(): MetadataRoute.Sitemap {
const now = new Date()
return [
{ url: `${BASE_URL}/`, lastModified: now, ... },
{ url: `${BASE_URL}/tools`, lastModified: now, ... },
{ url: `${BASE_URL}/classes`, lastModified: now, ... },
// ...도구 페이지 전체도 동일
]
}정적 페이지와 도구 페이지의 `lastModified`에 전부 `now`를 박아 두고 있었다. 바꾼 적도 없는 `/`, `/tools`, `/classes`가 sitemap 요청이 들어올 때마다 "방금 수정됨"으로 나가고 있었다는 뜻이다.
이게 왜 문제일까?
`lastmod`는 sitemaps.org 스펙에 정의된 "페이지가 마지막으로 수정된 시각" 을 가리키는 필드다. 크롤러가 이 값을 참고해 "이 URL은 이전에 본 버전과 달라졌는지"를 판단한다.
구글은 `lastmod`에 대해 비교적 구체적으로 입장을 밝혀 두었다. Googlebot은 `lastmod`가 정확하고 일관되게 제공될 때만 이 값을 신뢰한다. 페이지가 실제로는 바뀌지 않았는데 `lastmod`만 계속 갱신되면, 구글은 해당 사이트의 `lastmod` 값 자체를 점점 무시한다.
즉, 모든 URL에 현재 시각을 찍어 보내는 것은 "매번 다 바뀌었어요"라고 외치는 것과 같고, 그 결과는 더 자주 크롤링되는 것이 아니라 `lastmod` 신호 자체가 버려지는 것이다. 자주 업데이트되는 가이드 페이지의 진짜 변경 신호까지 같이 묻히게 된다는 뜻이다. 내 경우 더 나쁜 점은, sitemap이 요청 시점에 생성되다 보니 같은 배포 버전 안에서도 요청마다 `lastmod`가 달랐다는 것이다.
크롤러가 어제 받은 sitemap과 오늘 받은 sitemap을 비교했을 때 모든 URL의 `lastmod`가 바뀌어 있다. 이건 신호가 아니라 노이즈다.
검색엔진은 sitemap을 어떻게 쓰는가
오해하기 쉬운 부분을 정리해 두면 다음과 같다.
sitemap은 크롤링 대상 URL의 후보 목록을 알려주는 힌트다. 랭킹에 직접 영향을 주는 도구가 아니다.
`lastmod`는 크롤러가 재방문 스케줄을 짤 때 참고하는 신호다. 바뀌었다고 표시된 URL을 우선적으로 다시 보는 식이다.
단, 이 참고는 신뢰가 쌓였을 때만 이루어진다. 신뢰가 깨지면 해당 사이트의 `lastmod`는 통째로 무시된다.
`changefreq`, `priority`는 구글이 공식적으로 사용하지 않는다고 명시한 필드다. 넣어도 무해하지만 의미 있는 효과는 기대하지 않는 것이 맞다.
결국 sitemap에서 우리가 통제할 수 있는 유의미한 신호는 사실상 URL 목록과 정확한 `lastmod` 두 가지뿐이라고 봐도 크게 틀리지 않는다. 그 둘 중 하나를 스스로 망가뜨리고 있었던 셈이다.
문제 해결
원칙은 단순하다. "실제로 바뀌었을 때만 `lastmod`를 바꾼다."
정적 페이지(`/`, `/tools`, `/classes` 등): 콘텐츠가 코드에 박혀 있으므로, 해당 페이지를 실제로 수정한 배포 날짜를 상수로 고정한다.
원래부터 DB의 `updatedAt`을 쓰고 있었고, 이건 그대로 두었다. 이게 원래 `lastmod`가 쓰여야 할 올바른 형태다.
// src/lib/sitemap-metadata.ts
export const STATIC_PAGE_LAST_MODIFIED = new Date('2026-04-17')
export const TOOL_LAST_MODIFIED: Record = {
'damage-calculator': new Date('2026-03-22'),
'gem-simulator': new Date('2026-04-10'),
// ...
}`lastModified: new Date()` 는 코드만 보면 자연스럽고 "항상 최신"처럼 보이지만, sitemap의 맥락에서는 "이 필드의 의미를 스스로 파괴하는 코드" 였다.