Next.js App Router 동적 사이트맵(Sitemap) 생성하기

nagosu·2025년 2월 16일
0
post-thumbnail

사이트맵(Sitemap)이란?

사이트맵(Sitemap)은 검색 엔진이 웹사이트의 구조를 쉽게 이해할 수 있도록 도와주는 파일(XML 형식)이다.

https://example.com/sitemap.xml 같은 형태로 제공되고

sitemap.xml 파일을 통해 검색엔진 크롤링 최적화SEO 향상 효과를 가져다 준다.


정적 사이트맵(Sitemap) 방식

흔히 알고 있는 sitemap.xml 파일은 정적 사이트맵 방식에 해당한다.

직접 sitemap.xml을 생성해 파일 내부에 URL, 최근 수정일, 중요도 등을 지정하는 방식이다.

sitemap.xml 예시

<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://example.com/</loc>
    <lastmod>2024-02-16</lastmod>
    <priority>1.0</priority>
  </url>
  <url>
    <loc>https://example.com/about</loc>
    <lastmod>2024-02-10</lastmod>
    <priority>0.8</priority>
  </url>
  <url>
    <loc>https://example.com/blog</loc>
    <lastmod>2024-02-12</lastmod>
    <priority>0.7</priority>
  </url>
  <url>
    <loc>https://example.com/blog/nextjs-seo</loc>
    <lastmod>2024-02-10</lastmod>
    <priority>0.6</priority>
  </url>
  <url>
    <loc>https://example.com/blog/app-router-sitemap</loc>
    <lastmod>2024-02-12</lastmod>
    <priority>0.6</priority>
  </url>
</urlset>

이 방식은 사이트의 구조가 거의 변하지 않거나 URL이 많지 않을 때 유용하지만, 다음과 같은 한계가 있다.

✅ 정적 사이트맵이 적절한 경우

  • 사이트 구조가 거의 변하지 않을 때

  • URL이 많지 않고, 수동을 관리할 수 있을 때

❌ 정적 사이트맵의 한계

  • URL이 많아질 수록 관리가 어렵다.

  • 새로운 URL이 추가될 때마다 sitemap.xml 파일을 직접 수정해야 한다.

  • 깊은 URL 구조를 가진 사이트는 유지보수가 어렵다.


동적 사이트맵(Sitemap) 방식

현재 Next.js 14.2.4 버전의 App Router 방식을 기준으로,

app 폴더에 sitemap.ts 파일을 작성하면 Next.js가 이를 자동으로 읽어 sitemap.xml을 동적으로 생성해준다.

이 방식에선 fetch()를 사용하여 DB 또는 API에서 동적으로 페이지 목록을 가져올 수 있다.

Next.js App Router 동적 사이트맵 생성 코드 예시

1. API 엔드포인트 목록 정의

const apiEndpoints = [
  { path: "/example-path", api: `https://api.sample.com/v1/example` },
  { path: "/sample-path", api: `https://api.sample.com/v1/sample` },
  { path: "/test-data", api: `https://api.sample.com/v1/test` },
];

나는 페이지네이션이 있는 페이지들의 전체 페이지 개수를 가져오는 API 목록을 배열로 정의했다.

path는 사이트에서 실제 URL 경로이고, api는 페이지 개수를 가져오는 API URL이다.

예를 들어, /example-path의 totalPages 값이 5라면

/example-path?page=1 ~ /example-path?page=5 까지 사이트맵에 포함된다.

2. API 응답 타입 정의

interface ApiResponse {
  totalPages: number;  // 전체 페이지 개수
}

API 응답에선 전체 페이지 개수(totalPages)를 가져온다.

3. sitemap 함수 생성

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {

Next.js에서 제공하는 Metadata API를 사용해 sitemap.xml을 반환하는 함수이다.

정적 페이지와 동적 API 데이터를 활용해 페이지를 조합하여 사이트맵을 생성한다.

4. 정적 페이지 URL 생성

const BASE_URL = "https://example.com"; // BASE 도메인

const staticUrls = ["/", "/home", "/contact"];
const staticSitemap = staticUrls.map((url) => ({
  url: `${BASE_URL}${url}`,
  lastModified: new Date(),
}));

사이트의 기본 도메인을 BASE_URL로 설정하고,

정적 페이지 목록을 staticUrls 배열에 정의해주었다.

그리고 각 URL을 사이트맵 형식으로 변환해준다. (url + lastModified)

  • 추가적으로 changefreq(변경 빈도), priority(상대적 중요도)를 설정해줄 수도 있다.

참고 -> Sitemap XML 형식

5. 동적 페이지네이션 처리

const paginatedRoutes = await Promise.all(
  apiEndpoints.map(async ({ path, api }) => {
    try {
      const response = await fetch(api, { method: "GET" });
      const data: ApiResponse = await response.json();

      if (!data.totalPages) {
        console.warn("totalPages 값이 없습니다.", path);
        return { path, totalPages: 1 }; // 기본값
      }

      return { path, totalPages: data.totalPages };
    } catch (err) {
      console.error(err);
      return { path, totalPages: 1 }; // 기본값
    }
  }),
);

각 API를 요청해 totalPages(전체 페이지 개수) 값을 가져온다.

API 요청이 실패할 경우 기본값(totalPages = 1)을 반환해 오류를 방지한다.

6. 페이지네이션된 URL을 사이트맵에 추가

const paginatedSitemap = paginatedRoutes.flatMap(({ path, totalPages }) =>
  Array.from({ length: totalPages }, (_, i) => ({
    url: `${BASE_URL}${path}?page=${i + 1}`,
    lastModified: new Date(),
  })),
);

totalPages 값을 기반으로 ?page=1, ?page=2 등으로 URL을 생성한다.

이 코드는 모든 동적 페이지에 대해 사이트맵 URL을 자동으로 생성해준다.

결과 예시

<url>
  <loc>https://example.com/example-path?page=1</loc>
  <lastmod>2024-02-16</lastmod>
</url>
<url>
  <loc>https://example.com/example-path?page=2</loc>
  <lastmod>2024-02-16</lastmod>
</url>
...

7. 최종 사이트맵 반환

return [...staticSitemap, ...paginatedSitemap];

마지막으로 정적 URL과 동적 URL을 결합해 최종 사이트맵을 return 해준다.

이 함수가 실행되면 이제 sitemap.xml이 자동 생성된다.


전체 코드

import { MetadataRoute } from "next";

// API 엔드포인트 목록 예시
const apiEndpoints = [
  { path: "/example-path", api: `https://api.sample.com/v1/example` },
  { path: "/sample-path", api: `https://api.sample.com/v1/sample` },
  { path: "/test-data", api: `https://api.sample.com/v1/test` },
];

// Api 응답 타입 예시
interface ApiResponse {
  totalPages: number;
}

// 동적 사이트맵을 생성하는 sitemap 함수 예시
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const BASE_URL = "https://example.com"; // BASE 도메인

  // Static 페이지
  const staticUrls = ["/", "/home", "/contact"];
  const staticSitemap = staticUrls.map((url) => ({
    url: `${BASE_URL}${url}`,
    lastModified: new Date(),
  }));

  // Pagination 페이지
  const paginatedRoutes = await Promise.all(
    apiEndpoints.map(async ({ path, api }) => {
      try {
        const response = await fetch(api, { method: "GET" });
        const data: ApiResponse = await response.json();

        if (!data.totalPages) {
          console.warn("totalPages 값이 없습니다.", path);
          return { path, totalPages: 1 }; // 기본값
        }

        return { path, totalPages: data.totalPages };
      } catch (err) {
        console.error(err);
        return { path, totalPages: 1 }; // 기본값
      }
    }),
  );

  // Pagination 페이지 URL 생성
  const paginatedSitemap = paginatedRoutes.flatMap(({ path, totalPages }) =>
    Array.from({ length: totalPages }, (_, i) => ({
      url: `${BASE_URL}${path}?page=${i + 1}`,
      lastModified: new Date(),
    })),
  );

  return [...staticSitemap, ...paginatedSitemap];
}

동적 사이트맵 방식의 장점

1. 사이트맵 관리 자동화

  • 새로운 페이지가 추가될 때마다 자동으로 sitemap.xml이 갱신된다.

2. 검색 엔진 최적화(SEO) 향상

  • 검색 엔진이 최신 URL을 빠르게 인덱싱할 수 있게 도와준다.

참고자료

profile
프론트엔드 개발자..일걸요?

0개의 댓글