RSS듀오랩스
Next.js

Next.js 사이트맵과 메타데이터 라우트 캐싱

작성자
듀오랩스 대표·6분 읽기

글을 발행하면 블로그의 RSS 에는 몇 초 만에 새 글이 들어오는데, 사이트맵에는 며칠째 들어오지 않았습니다. 응답 헤더의 age 값만 계속 올라갔습니다. 발행할 때 캐시 태그를 무효화하고 있었고, 그 태그는 글 목록과 RSS 가 함께 쓰는 태그였습니다. RSS 는 갱신되는데 사이트맵만 멈춰 있었습니다.

sitemap.ts 도 페이지처럼 다시 만들어진다는 생각

Next.js 앱 라우터에서는 app/sitemap.ts 파일 하나로 사이트맵을 만듭니다. 파일 안에서 데이터를 읽어 주소 목록을 돌려주면 됩니다. 페이지와 같은 폴더에 있고 같은 방식으로 데이터를 읽으니, 캐시도 페이지와 같은 방식으로 관리된다고 여기기 쉽습니다.

sitemap 문서는 이 파일의 정체를 다르게 적습니다.

sitemap.js is a special Route Handler that is cached by default unless it uses a Request-time API or dynamic config option.

사이트맵은 페이지가 아니라 특수한 라우트 핸들러이고, 요청 시점 API 를 쓰거나 동적 설정을 주지 않는 한 기본적으로 캐시됩니다. 같은 설명이 robots.ts, opengraph-image.tsx 같은 다른 메타데이터 파일에도 해당합니다.

태그 무효화가 닿지 않은 곳

이 블로그에서 사이트맵이 멈춘 원인도 여기 있었습니다. 발행할 때 부르는 무효화는 글 목록과 RSS 가 쓰는 캐시 태그를 비웠고, 그 두 곳은 잘 갱신됐습니다. 사이트맵은 메타데이터 라우트로서 자기 캐시 항목을 따로 갖고 있어서, 태그나 경로를 무효화해도 그 항목에는 닿지 않았습니다.

당시 시도했던 방법과 결과는 코드 주석에 남아 있습니다. revalidatePath 를 사이트맵 경로에 걸어 봤지만 발행 직후에도 사이트맵의 age 는 계속 올라갔습니다.

그래서 방향을 바꿨습니다. 사이트맵을 메타데이터 파일 대신 직접 만든 라우트 핸들러(app/sitemap.xml/route.ts)로 옮기고, 응답에 s-maxage 를 직접 붙여 신선도를 제어했습니다. RSS, Atom, JSON 피드가 이미 라우트 핸들러였으므로 형제 파일들과 같은 모양이 됐습니다. 이제 사이트맵의 갱신 주기는 우리가 응답 헤더로 정합니다.

메타데이터 파일 규칙을 쓰지 말라는 이야기는 아닙니다. 글이 자주 바뀌지 않는 사이트라면 기본 캐시로 충분합니다. 다만 발행 즉시 반영이 요구된다면, 그 파일이 페이지와 다른 캐시 체계에 있다는 점을 알고 시작해야 합니다.

사이트맵이 커질 때

문서는 사이트맵을 나누는 방법도 안내합니다. generateSitemaps 함수로 여러 개를 만들고, 각 사이트맵에 id 를 붙입니다.

export async function generateSitemaps() {
  return [{ id: 0 }, { id: 1 }, { id: 2 }];
}

export default async function sitemap({ id }) {
  const start = id * 50000;
  const posts = await getPosts(start, start + 50000);
  return posts.map((post) => ({ url: `${SITE}/${post.slug}`, lastModified: post.updatedAt }));
}

문서는 예제 주석으로 Google 의 한도가 사이트맵 하나당 URL 5만 개라고 적습니다. 이 규모라면 사이트맵 하나를 만드는 조회 자체가 무거워지므로, 캐시 전략을 정하지 않은 채로 두기 어렵습니다.

사이트맵에 무엇을 넣을지도 결정이다

기술적인 캐시 문제와 별개로, 사이트맵에 넣을 주소를 고르는 것도 결정입니다. 이 블로그는 한동안 태그 페이지를 전부 사이트맵에 넣고 있었습니다. 글이 186편일 때 사이트맵 URL 이 690개였고, 그중 485개가 태그 페이지였습니다. 그때 기준을 정해 글이 세 편 이상인 태그만 사이트맵에 남기고 나머지는 noindex, follow 로 돌렸습니다. 태그 페이지 50개가 남았습니다.

사이트맵은 「이 사이트에 있는 모든 주소」가 아니라 「검색엔진이 가져가길 바라는 주소」의 목록이라고 봅니다. 그 정리 과정은 태그 485개를 전부 색인시키고 있었습니다에 따로 적어 두었습니다.

메타데이터 파일을 쓸 때 확인할 것

확인 이유
이 파일이 캐시되는가 메타데이터 라우트는 기본적으로 캐시된다
발행 즉시 반영이 필요한가 필요하면 라우트 핸들러로 만들고 헤더로 제어
무효화가 실제로 닿는가 태그나 경로 무효화 뒤 age 헤더를 직접 확인
목록에 넣을 주소를 골랐는가 사이트맵은 전체 주소 목록이 아니다

세 번째 줄이 이번 일에서 얻은 습관입니다. 무효화를 호출했다는 것과 대상이 실제로 갱신됐다는 것은 다른 사실이라, 발행 뒤 curl -Iage 가 0 으로 돌아오는지 보는 편이 확실합니다.

여기까지가 확실한 부분

sitemap.js 가 기본적으로 캐시되는 특수 라우트 핸들러라는 점, generateSitemaps 로 사이트맵을 나누는 방법과 사이트맵당 5만 URL 한도 언급은 Next.js 16 문서 기준입니다. 발행 직후 사이트맵이 갱신되지 않아 라우트 핸들러로 옮긴 것과 태그 페이지를 485개에서 50개로 줄인 것은 이 블로그에서 실제로 한 조치이고, 시도한 방법과 결과는 저장소의 코드 주석에 기록돼 있습니다. 메타데이터 라우트의 캐시 항목에 태그 무효화가 닿지 않는 동작이 어느 버전까지 그대로인지는 확인하지 않았습니다.

이 게시글 공유하기

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.