기술 블로그를 만들 때 sitemap.xmlrobots.txt는 보통 마지막에 대충 붙이는 파일처럼 다뤄집니다. 하지만 실제로는 검색 엔진이 사이트를 이해하는 가장 기본적인 신호 중 일부입니다. Google은 sitemap에 포함된 URL을 “검색 결과에 보여주고 싶은 canonical URL”로 보라고 설명하고, robots.txt는 특정 호스트에서 어떤 경로를 크롤링할 수 있는지 알려주는 규칙 파일이라고 설명합니다. 둘 다 작고 단순한 파일이지만, 내용이 부정확하면 사이트 전체 구조가 흐려질 수 있습니다.

Next.js App Router를 쓰면 이 두 파일을 손으로 유지하지 않아도 됩니다. 공식 문서에 따르면 sitemap.(xml|js|ts)robots.(txt|js|ts)는 메타데이터 파일 규칙으로 취급되며, 함수가 반환한 값을 바탕으로 실제 XML과 텍스트 출력이 생성됩니다. 그래서 정적 블로그를 운영하는 입장에서는 “공개된 페이지 목록”과 “사이트 기준 URL”만 잘 관리하면, 발행할 때마다 자동으로 최신 상태를 유지할 수 있습니다.

이 글에서는 현재 이 블로그 레포처럼 App Router와 정적 export를 사용하는 프로젝트를 기준으로, sitemap.xmlrobots.txt를 자동 생성하는 가장 실용적인 구성을 정리해보겠습니다.

검색/신뢰색인 기본기

Next.js 블로그에 sitemap.xml과 robots.txt를 자동 생성하는 방법

Next.js App Router에서 `sitemap.ts`와 `robots.ts`를 이용해 검색 엔진용 기본 파일을 자동 생성하는 방법을 정리합니다. 정적 블로그 기준으로 무엇을 포함하고 무엇을 빼야 하는지도 함께 설명합니다.

핵심 1

먼저 정리: sitemap과 robots는 역할이 다릅니다

핵심 2

Next.js App Router에서는 파일을 직접 만들지 않아도 된다

핵심 3

먼저 사이트 기준 URL을 한 곳에서 관리한다

색인 기본기검색/신뢰automatic sitemap robots nextjs
사이트 URL, 정적 페이지, 발행된 글 목록을 바탕으로 sitemap.xml과 robots.txt를 생성하는 흐름

먼저 정리: sitemap과 robots는 역할이 다릅니다

먼저 둘을 같은 종류의 파일로 생각하지 않는 편이 좋습니다.

  • sitemap.xml: 검색 엔진에게 “이 사이트에서 대표로 봐야 할 공개 URL은 이것들입니다”라고 알려주는 파일
  • robots.txt: 크롤러에게 “이 호스트에서 어떤 경로를 읽어도 되는지, 안 되는지”를 알려주는 파일

Google의 sitemap 문서도 sitemap에 넣는 URL은 canonical URL이어야 한다고 설명합니다. 반면 robots 문서는 이 파일이 사이트 루트에 위치하며, 해당 프로토콜과 호스트, 포트에만 적용된다고 설명합니다. 즉 sitemap은 “무엇을 보여줄지”에 가깝고, robots는 “어디를 읽게 할지”에 가깝습니다.

이 차이를 이해하면 어떤 실수들을 피해야 하는지도 보입니다.

  • sitemap에 초안 페이지나 중복 URL을 넣으면 안 됩니다.
  • robots.txt로 canonical을 대신하려고 하면 안 됩니다.
  • www와 apex가 섞여 있는데 sitemap에 둘 다 넣으면 안 됩니다.

이전 글에서 정리한 www, non-www, canonical 정리 글과 연결되는 지점이 바로 여기입니다. sitemap과 robots도 대표 주소 기준으로 한 방향을 봐야 합니다.

Next.js App Router에서는 파일을 직접 만들지 않아도 된다

Next.js 공식 문서는 app/sitemap.tsapp/robots.ts를 special metadata file로 취급합니다. 즉 우리가 함수만 내보내면 Next.js가 실제 /sitemap.xml/robots.txt 응답을 만들어줍니다.

이게 좋은 이유는 단순합니다.

  • 글이 늘어날 때마다 XML을 손으로 수정하지 않아도 됩니다.
  • 사이트 URL이 바뀌면 한 곳만 수정하면 됩니다.
  • 초안과 발행 글을 코드에서 분기하기 쉽습니다.
  • 정적 export 프로젝트에서도 결과물이 자연스럽게 생성됩니다.

특히 1인 개발 블로그에서는 “파일을 수동으로 유지하는 비용”보다 “발행 상태를 코드에서 진실의 원천으로 두는 편의성”이 훨씬 큽니다.

1. 먼저 사이트 기준 URL을 한 곳에서 관리한다

sitemap과 robots를 자동 생성할 때 가장 먼저 필요한 건 사이트의 기준 URL입니다. https://example.com인지, https://www.example.com인지, 로컬호스트인지가 흔들리면 생성되는 결과도 전부 흔들립니다.

이 레포에서는 src/lib/site-url.ts에서 기준 URL을 이렇게 계산하고 있습니다.

const FALLBACK_SITE_URL = "http://localhost:3000";

export function getSiteUrl() {
  const rawSiteUrl =
    process.env.NEXT_PUBLIC_SITE_URL ?? process.env.VERCEL_PROJECT_PRODUCTION_URL;

  if (!rawSiteUrl) {
    return FALLBACK_SITE_URL;
  }

  return rawSiteUrl.startsWith("http") ? rawSiteUrl : `https://${rawSiteUrl}`;
}

이 패턴이 좋은 이유는 명확합니다.

  • 개발 환경에서는 로컬 주소를 사용합니다.
  • 운영 환경에서는 실제 도메인을 우선합니다.
  • sitemap, robots, canonical, Open Graph가 같은 기준 URL을 공유할 수 있습니다.

여기서 중요한 건 운영 환경에서 NEXT_PUBLIC_SITE_URL에 대표 도메인을 명시해 두는 것입니다.

NEXT_PUBLIC_SITE_URL=https://www.example.com

이렇게 해야 sitemap이 프리뷰 주소나 임시 주소를 기준으로 만들어지는 일을 줄일 수 있습니다.

2. sitemap은 “공개된 페이지 목록”만 반환하게 만든다

Google은 sitemap을 만들 때 canonical URL만 포함하라고 안내합니다. 즉 존재하는 URL을 무조건 많이 넣는 것이 목적이 아닙니다. 실제로 검색 결과에 보여줘도 되는 공개 페이지들만 넣는 편이 맞습니다.

이 블로그에서는 그래서 정적 페이지와 발행된 글만 합쳐서 sitemap을 만들도록 구성했습니다.

import type { MetadataRoute } from "next";
import { getPublishedArticles } from "@/lib/article-content";
import { getSiteUrl } from "@/lib/site-url";

const siteUrl = getSiteUrl();
export const dynamic = "force-static";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const staticRoutes = ["", "/blog", "/about", "/contact", "/privacy", "/terms"];
  const articles = await getPublishedArticles();

  const staticEntries = staticRoutes.map((route) => ({
    url: `${siteUrl}${route}`,
    lastModified: new Date(),
  }));

  const postEntries = articles.map((article) => ({
    url: `${siteUrl}/blog/${article.slug}`,
    lastModified: new Date(article.updatedAt ?? article.publishedAt),
  }));

  return [...staticEntries, ...postEntries];
}

이 코드에서 중요한 포인트는 세 가지입니다.

발행된 글만 넣는다

아직 본문이 없는 글, noindex가 붙은 준비 페이지, 내부 실험 페이지를 sitemap에 넣지 않습니다. 현재 이 레포는 getPublishedArticles()를 통해 실제 마크다운 파일이 존재하는 글만 수집하므로, 그 조건을 자연스럽게 만족합니다.

lastModified를 실제 발행/수정일 기준으로 둔다

Next.js 공식 문서의 예시처럼 lastModified를 함께 반환할 수 있습니다. 글 기반 사이트라면 이 값은 지금 시각보다 updatedAt 또는 publishedAt에서 가져오는 편이 더 자연스럽습니다.

정적 페이지와 글 페이지를 분리해 관리한다

홈, 아카이브, About, Contact 같은 정적 페이지는 코드에서 배열로 관리하고, 글 페이지는 콘텐츠 저장소에서 가져옵니다. 이렇게 나누면 누락을 줄이기 쉽습니다.

3. robots.txt는 작고 단순하게 유지하는 편이 좋다

Google robots 문서는 robots.txt가 사이트 루트에 있어야 하고, 그 호스트에만 적용된다고 설명합니다. 즉 https://www.example.com/robots.txtwww.example.com에 적용되고, https://example.com/robots.txt와는 별개입니다. 이 점 때문에도 대표 도메인 정리가 중요합니다.

Next.js에서는 app/robots.ts에서 MetadataRoute.Robots 객체를 반환하면 됩니다.

이 레포의 현재 구현은 이렇게 되어 있습니다.

import type { MetadataRoute } from "next";
import { getSiteUrl } from "@/lib/site-url";

const siteUrl = getSiteUrl();
export const dynamic = "force-static";

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
    },
    sitemap: `${siteUrl}/sitemap.xml`,
  };
}

이 정도면 공개용 기술 블로그의 기본값으로 꽤 좋습니다.

  • 모든 크롤러에 공개 경로를 허용
  • sitemap 위치를 명시
  • 별도 비공개 디렉터리가 없으므로 과하게 막지 않음

특히 초반 블로그는 robots.txt를 복잡하게 쓰다가 스스로 발목을 잡는 경우가 많습니다. 공개할 페이지가 중심이라면, 기본값은 단순해야 합니다.

4. 무엇을 robots에서 막고, 무엇을 sitemap에서 빼야 하는가

이 부분을 혼동하면 구조가 바로 흐려집니다.

제가 기본적으로 분리하는 기준은 이렇습니다.

  • 검색과 크롤링이 필요 없는 내부 경로: robots에서 disallow 검토
  • 공개는 되지만 대표 URL이 아닌 페이지: sitemap에서 제외
  • 아직 발행되지 않은 페이지: sitemap 제외, 필요하면 noindex

예를 들어 이런 경로가 있다면:

/private/
/draft-preview/
/internal-tools/

robots는 이렇게 쓸 수 있습니다.

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: "*",
      allow: "/",
      disallow: ["/private/", "/draft-preview/"],
    },
    sitemap: "https://www.example.com/sitemap.xml",
  };
}

하지만 여기서도 주의할 점이 있습니다. sitemap에서 뺄 URL과 robots에서 막을 URL은 꼭 같을 필요가 없습니다. 예를 들어 /tag/ 아카이브를 공개는 하되 대표 URL로 색인시키고 싶지 않다면, robots로 막기보다는 sitemap에서만 빼는 편이 더 맞을 수 있습니다. 반대로 정말 내부용 경로라면 robots에서 아예 막는 편이 낫습니다.

5. 블로그에서는 sitemap에 넣을 URL을 적게 고르는 편이 오히려 좋다

Google 문서는 몇 개 안 되는 작은 사이트라면 수동 sitemap도 가능하지만, URL이 조금만 늘어나면 사이트 소프트웨어가 자동 생성하는 편이 가장 좋다고 설명합니다. 이 문서를 블로그 운영 관점으로 바꿔 보면 중요한 의미가 있습니다. sitemap은 “최대한 많이 넣는 목록”이 아니라, 구조를 깨끗하게 유지한 “대표 URL 목록”이어야 한다는 점입니다.

그래서 저는 블로그에서 보통 아래만 넣습니다.

  • 아카이브 메인
  • 필수 정책/신뢰 페이지
  • 발행된 글

그리고 아래는 신중하게 봅니다.

  • 빈 카테고리 페이지
  • 필터 조합 URL
  • 검색 결과 페이지
  • 아직 본문이 비어 있는 예정 글

이전 글에서 이야기한 slug, 날짜, 카테고리 구조 글과도 연결되는데, 탐색용 구조를 다 URL로 열어두더라도 전부 sitemap에 넣을 필요는 없습니다. 오히려 대표성이 높은 페이지만 남기는 편이 깔끔합니다.

6. changeFrequencypriority는 선택 사항이다

Next.js sitemap.ts는 각 URL에 changeFrequencypriority를 넣을 수 있습니다. 공식 문서 예시도 이 필드를 보여줍니다. 다만 블로그 초기에 이 값을 지나치게 세밀하게 조정하는 데 시간을 많이 쓸 필요는 없다고 생각합니다.

필요하다면 이렇게 쓸 수 있습니다.

return [
  {
    url: `${siteUrl}/`,
    lastModified: new Date(),
    changeFrequency: "weekly",
    priority: 1,
  },
  {
    url: `${siteUrl}/blog`,
    lastModified: new Date(),
    changeFrequency: "daily",
    priority: 0.8,
  },
];

하지만 이 값보다 더 중요한 건 다음입니다.

  • URL이 대표 주소인가
  • 실제 공개 상태와 일치하는가
  • canonical과 호스트가 일관적인가

priority를 예쁘게 넣는 것보다, 잘못된 URL을 sitemap에서 빼는 편이 훨씬 중요합니다.

7. 정적 export 프로젝트에서는 빌드 후 실제 파일을 확인해 보는 게 좋다

이 레포처럼 output: "export"를 사용하는 프로젝트에서는 npm run build 이후에 정적 출력 결과에도 sitemap.xmlrobots.txt가 잘 생성되는지 확인하는 편이 좋습니다. Next.js가 메타데이터 파일을 생성하더라도, 배포 환경에서 기대한 경로로 나오는지 한 번은 직접 보는 게 안전합니다.

확인 순서는 아주 단순합니다.

npm.cmd run build
Get-ChildItem out

그 다음 브라우저나 터미널에서 아래를 확인합니다.

curl.exe http://localhost:3000/sitemap.xml
curl.exe http://localhost:3000/robots.txt

혹은 정적 서빙으로 확인할 수도 있습니다.

npx.cmd serve out

이 단계에서 봐야 하는 건 이런 것들입니다.

  • sitemap URL이 실제 도메인을 기준으로 나오는가
  • 초안 글이 빠져 있는가
  • robots가 루트에서 열리는가
  • sitemap 위치가 robots에 정확히 적혀 있는가

8. Search Console 제출은 “마지막 단계”이지 “대신 고쳐주는 단계”는 아니다

Google sitemap 문서는 sitemap 제출이 단지 힌트일 뿐이며, 제출했다고 해서 Google이 반드시 다운로드하거나 URL을 모두 크롤링하는 것은 아니라고 설명합니다. 이 말은 꽤 중요합니다. Search Console에 제출하는 행위 자체가 사이트 구조를 고쳐주지는 않는다는 뜻입니다.

그래서 저는 순서를 이렇게 잡습니다.

  1. 자동 생성 코드 완성
  2. 로컬과 배포 환경에서 실제 출력 확인
  3. 대표 호스트와 canonical 정리 확인
  4. 그 다음 Search Console 제출

이 순서가 맞아야 제출 이후의 진단도 쉬워집니다.

제가 실제로 쓰는 체크리스트

블로그에서 sitemap.xmlrobots.txt를 자동화한 뒤에는 보통 아래만 다시 봅니다.

  1. 사이트 기준 URL이 한 곳에서 관리되는가
  2. sitemap에 대표 URL만 들어가는가
  3. 발행되지 않은 글은 sitemap에서 빠지는가
  4. robots가 루트 경로에서 열리는가
  5. robots의 sitemap 경로가 실제 도메인과 일치하는가
  6. canonical과 sitemap 호스트가 같은가
  7. Search Console에 제출할 준비가 되었는가

이 정도만 맞아도 작은 기술 블로그는 검색 엔진이 구조를 이해하는 데 필요한 기본 신호를 꽤 안정적으로 갖추게 됩니다.

마무리

Next.js App Router에서 sitemap.tsrobots.ts를 쓰면 검색 엔진용 기본 파일을 수동으로 관리할 필요가 크게 줄어듭니다. 중요한 건 파일을 자동으로 “생성한다”는 사실보다, 어떤 URL을 생성하고 어떤 URL을 제외할지에 대한 기준을 코드에 녹여두는 것입니다.

제 기준에서는 좋은 기본값이 이렇습니다. sitemap에는 대표 공개 URL만 넣고, robots는 작고 명확하게 유지하며, 사이트 기준 URL은 환경 변수와 유틸 함수 한 곳에서 관리합니다. 그리고 발행되지 않은 글이나 준비 페이지는 sitemap에서 빼서 사이트 구조가 실제 공개 상태와 다르게 보이지 않게 합니다.

이 흐름을 제대로 잡아두면 이후 Search Console 점검, 색인 문제 진단, canonical 정리까지 한 줄로 이어집니다. 다음 단계에서는 보통 Search Console에 사이트를 등록한 뒤 어떤 항목을 먼저 봐야 하는지가 자연스럽게 이어집니다.

참고 문서