login
nextjs
on-demande-isr
SEO
sitemap
next-sitemap
On-Demand ISR을 사용하는 블로그의 sitemap 생성하기2023-05-25

블로그의 SEO를 위해 서치엔진 크롤러에게 사이트 정보를 제공하기 위한 robots.txt와 sitemap.xml의 추가가 필요했다.

페이지 수가 적은 블로그이기 때문에 하드코딩으로 작성해도 금방이지만, 이를 자동화 해주고 동적인 페이지들의 사이트맵 생성도 쉽게 처리해주는 next-sitemap라이브러리를 프로젝트에 추가했다.

기본적으로 next-sitemap.config.js 파일을 생성하여 간단하게 설정을 추가하고, package.json에 "postbuild": "next-sitemap" 스크립트를 추가해주면 된다.

간단하다. 하지만..

정적으로 생성되는 페이지와 반 정적(?)으로 생성되는 페이지들이 섞여있는 이 블로그에서는 어떻게 사이트맵을 생성할 것인지 조금 더 고민해봐야 했다.

SSG + On-Demand ISR이 혼합된 포스팅 페이지들

현재 블로그의 페이지들은 기본적으로 SSG가 되고 있기 때문에, 앱 빌드 시점에 모든 페이지가 생성된다. 따라서 next-sitemap은 빌드 이후, 빌드 시점에 생성된 모든 페이지들이 포함된 sitemap.xml을 자동 생성할 수 있다. 정적으로 사이트맵 생성이 가능한 것이다.

그러나,

이 블로그는 on-demand ISR을 활용하여 재빌드 없이 새로운 포스팅을 생성할 수 있도록 구성되어 있다. 👉관련 포스팅: 블로그에 ISR, On-Demand ISR 적용하기

이 때문에 해결해야 할 문제가 발생한다.

이미 생성되어 데이터베이스에 저장되어 있는 포스팅들은 앱 빌드 후 정적 생성되는 sitemap.xml 파일에 포함이 된다. 하지만 나중에 ISR 방식으로 추가 작성한 포스팅은 당연히 빌드 시점에 생성된 sitemap.xml에 포함되지 않게 된다.

새로 포스팅을 작성한 이후에 다시 앱을 빌드하면 next-sitemap이 해당 포스팅 페이지가 추가된 sitemap을 생성해주겠지만, 애초에 포스팅을 작성할 때마다 앱을 재빌드 하지 않기 위해 채택한 ISR 아니었던가.

잠시 dynamic sitemaps에 대해..

크롤러는 도메인/sitemap.xml 경로에 접근하여 해당 사이트의 정보를 확인한다. 따라서 페이지를 요청하는 시점에 최신 페이지 정보를 담은 sitemap을 생성하여 응답해줄 수 있다면, 동적으로 sitemap을 생성하여 크롤러에게 제공할 수 있다.

‘요청 시 최신 데이터를 렌더링 한 페이지를 생성하여 응답’ 한다? SSR을 활용하면 되겠다.

기본적인 순서는 다음과 같다.

  1. sitemap.xml 페이지 생성
  2. getServerSideProps 함수 내부에서 DB로부터 포스팅 페이지 주소 목록(post id)을 fetch
  3. fetch 해온 데이터로 sitemap.xml 템플릿을 생성하여 응답

물론 next-sitemap이 조금 더 간편하게 해당 작업을 수행하도록 도와주지만, 원리를 파악해보고자 원론적인 방법으로 코드를 작성해보면 다음과 같을 것이다. (사이트맵 프로토콜 참고)

// pages/sitemaps.xml/index.tsx
const createUrl = (route) => `
    <url>
        <loc>https://42blog.vercel.app/posts/${route}</loc>
        // lastmod, priority etc..
    </url>
`;

export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
    const pageIds = await fetchPageIdsFromDB();

    const sitemap = `
        <?xml version="1.0" encoding="UTF-8"?>
            <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
                ${pageIds.map((id) => createUrl(id)).join("")}
            </urlset>
    `;

    res.setHeader("Content-Type", "text/xml");
    res.write(sitemap);
    res.end();
    
    return {
        props: {},
    };
}

export default function SiteMap() {}

이제 /sitemaps.xml 페이지 접속 시, 포스팅 상세 페이지 주소 목록을 모두 포함하여 동적으로 생성되는 sitemap을 확인할 수 있다.

중복 생성되는 Url 줄이기

이제 동적으로도 사이트맵을 생성할 수 있음을 알았다.

그렇다면 Home이나 About 처럼 정적 생성되는 페이지들에 대해서는 정적 사이트맵 생성이 필요할 것이고, 동적(on-demand ISR)으로 생성되는 포스팅 페이지들을 고려하여 SSR을 이용하여 동적 사이트맵 생성을 시키면 될 것이다.

이를 위한 next-sitemap 설정은 다음과 같다.

  1. 먼저 동적 사이트맵 생성을 위한 페이지를 만들어준다.

    위에서 구현한 것과 거의 동일하지만, next-sitemap 제공 메서드를 사용하여 조금 더 간편하게 구현 가능하다. 정적 사이트맵 파일과 구분하기 위해 페이지 이름을 dynamic-sitemap.xml 로 정했다.

// pages/dynamic-sitemap.xml/index.tsx
import { getServerSideSitemapLegacy } from 'next-sitemap';
import type { GetServerSideProps } from 'next';

const siteUrl = process.env.PRODUCTION_URL || 'https://42blog.vercel.app';

export const getServerSideProps: GetServerSideProps = async (ctx) => {
  const postIds = await fetchPageIdsFromDB();

  const fields = postIds.map(({ id }) => ({
    loc: `${siteUrl}/posts/${id}`,
    lastmod: new Date().toISOString(),
    priority: 0.7, // 자동 생성 시 부여되는 default priority 값
  }));

  return getServerSideSitemapLegacy(ctx, fields);
};

export default function SiteMap() {}
  1. 정적 페이지들을 위한 정적 사이트맵 자동 생성 설정을 위해 next-sitemap.config.js 파일 추가

    additionalSitemaps 에 위에서 추가한 동적 사이트맵 생성 페이지의 주소를 추가하여, 크롤러에게 해당 주소에 사이트맵이 추가적으로 존재함을 알려준다.

// next-sitemap.config.js
const siteUrl = process.env.PRODUCTION_URL || 'https://42blog.vercel.app';

/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl,
  changefreq: false,
  generateIndexSitemap: false,
  generateRobotsTxt: true,
  robotsTxtOptions: {
    policies: [
      {
        userAgent: '*',
        disallow: [], // 크롤링 되지 않아야 할 경로 배열
      }
    ],
    additionalSitemaps: [ `${siteUrl}/dynamic-sitemap.xml` ]
  },
};

단 2개의 파일을 추가하여 쉽게 완료된 것 같으나, 문제가 하나 있다.

  1. 앱 빌드 시 Home, About 페이지 외에도 포스팅 페이지들이 정적 생성(SSG) 되기 때문에, 해당 시점에 데이터베이스에 존재하는 포스팅들의 주소도 정적 사이트맵 목록에 추가된다.
  2. 이후 크롤러가 /dynamic-sitemap.xml 페이지에 접근하면, 사전에 작성해 둔 SSR 로직에 따라 데이터베이스로부터 모든 포스팅 목록을 가져와 동적 생성되는 사이트맵의 목록에 추가한다.

즉, 빌드 이후에 새롭게 추가되는 포스팅 페이지들을 위해 동적 사이트맵 생성을 가능하도록 했지만 이것 때문에 기존에 존재하던 포스팅 페이지들이 사이트맵에 중복으로 포함되게 된다.

해결 방법은 간단하다.

  1. 정적 사이트맵 생성 시 포스팅 페이지들을 생성 대상에서 제외한다.
  2. 포스팅 페이지들은 동적 사이트맵으로만 생성한다.

이를 위한 약간의 next-sitemap.config 설정 추가가 필요하다.

postbuild 시에 생성될 정적 사이트맵에 포스팅 페이지 경로(/posts/*)와 동적 사이트맵 생성 페이지 경로를 제외하도록 exclude 경로 목록을 더해준다.

// next-sitemap.config.js
const siteUrl = process.env.PRODUCTION_URL || 'https://42blog.vercel.app';

/** @type {import('next-sitemap').IConfig} */
module.exports = {
  siteUrl,
  changefreq: false,
  generateIndexSitemap: false,
  exclude: [
      '/dynamic-sitemap.xml',
      '/posts/*',
    ],  // 정적 사이트맵 생성 시 제외할 경로 배열
  generateRobotsTxt: true,
  robotsTxtOptions: {
    policies: [
      {
        userAgent: '*',
        disallow: [],
      }
    ],
    additionalSitemaps: [ `${siteUrl}/dynamic-sitemap.xml` ]
  },
};

결과물 확인

이제 성공적으로 사이트맵이 생성 됐는지 확인해보자.

먼저, 성공적으로 앱 build 및 next-sitemap의 postbuild가 이뤄졌다면 Next.js의 정적 파일 생성 경로인 /public 에 robots.txt 파일과 sitemap.xml 파일이 추가된다.

배포가 완료 됐다면 각각의 경로에 접속하여 의도한대로 해당 파일들이 작성 됐는지 확인해볼 수 있다.

(/posts/create, /posts/edit 페이지는 admin 기능이기 때문에 크롤링이 불가능하도록 disallow 경로로 추가해뒀다.)

피드백은 언제나 감사합니다.
오류 정정, 질문, 토론거리를 자유롭게 댓글로 남겨주세요.
블로그 소스코드에 대한 리뷰, PR도 적극 환영합니다!
► Github Repository 바로가기

댓글을 작성하려면 로그인이 필요합니다.