๊ธฐ์กด ํ๋ก ํธ์๋(Next.js) ์๋ฒ์ ์ฌ์ดํธ๋งต ์์ฑ ๋ก์ง์ ๋ฐฑ์๋์ ๊ฒ์๊ธ ๋ชฉ๋ก API๋ฅผ ํ์ฉํ๊ณ ์์๋ค. ์ด ๊ณผ์ ์์ ๋ช ๊ฐ์ง ๋ฌธ์ ๊ฐ ๋ฐ์ํ๋ค.
export const dynamic = 'force-static'; ์ค์ ์ ํตํด ๋น๋ ์์ ์ ์ฌ์ดํธ๋งต์ ๋ฏธ๋ฆฌ ์์ฑํ์ฌ ์๋ต ์๋๋ฅผ ์ต์ ํํ๋ค.
import { MetadataRoute } from "next";
// export const dynamic = 'force-dynamic'; ๊ธฐ์กด SSR ์ค์
export const dynamic = 'force-static'; // SSG ๋ฐฉ์์ผ๋ก ์ ํ
์ด ๋ฐฉ๋ฒ์ผ๋ก ์๋ต์๋๋ ํฌ๊ฒ ์ค์ผ ์ ์์์ง๋ง ์๋ก์ด ์ฝํ ์ธ ๊ฐ ์ถ๊ฐ๋ ๋๋ง๋ค ์ ์ฒด ํ๋ก์ ํธ๋ฅผ ์ฌ๋น๋ํด์ผ ํ๋ฏ๋ก ์ค์๊ฐ ๋ฐ์ ์ธก๋ฉด์์ ์ ์ฐ์ฑ์ด ๋จ์ด์ก๋ค.
์ฒ์์๋ sitemap ์์ฑ์ ์ํด ๋ณ๋์ ์ ์ฉ ์ปจํธ๋กค๋ฌ๋ฅผ ๋ง๋๋ ๊ฒ์ด ๊ณผ๋ํ๊ฒ ๋๊ปด์ ธ ๊ธฐ์กด ๊ฒ์๊ธ ๋ชฉ๋ก API๋ฅผ ๊ทธ๋๋ก ํ์ฉํ์๋ค.
๊ทธ๋ฌ๋ ํ์ด์ง ๊ธฐ๋ฐ API์ด๋ค ๋ณด๋ ์ ์ฒด ๋ฐ์ดํฐ๋ฅผ ์์ฐจ์ ์ผ๋ก ์ฌ๋ฌ ๋ฒ ํธ์ถํด์ผ ํ๊ณ ๋ณธ๋ฌธ ๊ฐ์ sitemap์ ๋ถํ์ํ ํ๋๊น์ง ํฌํจ๋์ด ์ค๋ฒํค๋๊ฐ ํฌ๊ณ ๊ธ ์๊ฐ ๋์ด๋ ์๋ก ์๋ต ์๋๊ฐ ๊ธ์ฆํ์๋ค.
๋ฐ๋ผ์ ๊ทผ๋ณธ์ ์ธ ํด๊ฒฐ์ ์ํด ๊ธฐ์กด ๋๋ฉ์ธ์ /sitemap ์๋ ํฌ์ธํธ๋ฅผ ์ถ๊ฐํ์๋ค.
@Query("""
SELECT new com.pyomin.cool.dto.SitemapDto(
p.slug,
COALESCE(p.updatedAt, p.createdAt)
)
FROM Post p
WHERE p.status = :status
ORDER BY COALESCE(p.updatedAt, p.createdAt) DESC, p.id DESC
""")
List<SitemapDto<String>> findAllForSitemap(PostStatus status);
sitemap ์์ฑ์ ํ์ํ ๋ฐ์ดํฐ๋ง ์ต์ํ์ผ๋ก ์กฐํํ๋๋ก DTO ํ๋ก์ ์
์ ์ฌ์ฉํ์๊ณ lastmod ์ค์ ์ ์ํด COALESCE()๋ฅผ ์ฌ์ฉํ์ฌ ์์ ์ผ์ด ์์ผ๋ฉด ์์ฑ์ผ์ ์กฐํํ๋๋ก ์ฒ๋ฆฌํ์๋ค. SitemapDto๋ ์๋์ ๊ฐ์ด ์ค๊ณํ์๋ค.
package com.pyomin.cool.dto;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
public record SitemapDto<K>(K key, Instant lastModified) {
public SitemapDto(K key, LocalDateTime lastModifiedLdt) {
this(
key,
lastModifiedLdt == null
? null
: lastModifiedLdt.atZone(ZoneId.of("Asia/Seoul")).toInstant()
);
}
}
๋ฌธ์์ด slug๋ฟ๋ง ์๋๋ผ ์ถํ์ ๋ค๋ฅธ ํ์
์ ์๋ณ์๋ ํฌํจํ ์ ์๋๋ก ์ ๋ค๋ฆญ์ผ๋ก ์ค๊ณํ์์ผ๋ฉฐ sitemap ํ์ค ํ๋์ธ <lastmod>์ W3C Datetime ํ์ค์ ์ถฉ์กฑํ๊ธฐ ์ํด DB์์ ์กฐํํ LocalDateTime์ ์๋น์ค ํ์์กด(Asia/Seoul) ๊ธฐ์ค์ผ๋ก ๋ณด์ ํ ํ Instant ํ์
์ผ๋ก ๋ณํํ๋ค.
์ดํ ์นดํ
๊ณ ๋ฆฌ๋ ๊ฐ์ ๋ฐฉ์์ผ๋ก ์๋ ํฌ์ธํธ๋ฅผ ์ค๊ณํ ํ ๊ฐ ๋๋ฉ์ธ๋ณ ์ฌ์ดํธ๋งต ๋ฐ์ดํฐ๋ฅผ ๋๊ธฐ์ ์ผ๋ก ๊ธฐ๋ค๋ฆฌ์ง ์๊ณ Promise.all()์ ์ฌ์ฉํ์ฌ ๋ณ๋ ฌ๋ก ํธ์ถํ๋ค.
๋ํ revalidate ์ต์
์ ํตํด ์ ํด์ง ์ฃผ๊ธฐ๋ง๋ค ์ฌ์ดํธ๋งต์ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์ฌ์์ฑํ๋๋ก ์ค์ ํ๋ค.
import { MetadataRoute } from "next";
import { getApiBase } from "@/lib/api/apiBase";
export const revalidate = 86400; // ISR (ํ๋ฃจ๋ง๋ค ์ฌ์์ฑ)
const SITE_URL = 'https://pyomin.com';
type SlugSitemap = { slug: string; lastModified: string };
type SitemapResponse<T> = { sitemap: T[] };
async function fetchPostSitemap(): Promise<SlugSitemap[]> {
const res = await fetch(`${getApiBase()}/posts/sitemap`);
if (!res.ok) throw new Error('posts/sitemap ์คํจ');
const json = (await res.json()) as SitemapResponse<SlugSitemap>;
return json.sitemap ?? [];
}
// ์นดํ ๊ณ ๋ฆฌ fetch ์๋ต
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const [posts, categories] = await Promise.all([fetchPostSitemap(), fetchCategorySitemap()]); // ๋์ ํธ์ถ
const postPages: MetadataRoute.Sitemap = posts.map((post) => ({
url: `${SITE_URL}/posts/${encodeURIComponent(post.slug)}`,
lastModified: post.lastModified,
changeFrequency: 'monthly',
priority: 0.8,
}));
// ์ด ์ธ ์ฌ์ดํธ๋งต ์์ฑ ๋ฐ return
}
๊ฒฐ๊ณผ์ ์ผ๋ก ์ฌ์ดํธ๋งต ์์ฑ ์๊ฐ์ 1์ด ์ด๋ด๋ก ๋จ์ถํ ์ ์์์ผ๋ฉฐ ์๋ต ์๋ ๊ฐ์ ์ ํตํด ๋ค์ด๋ฒ Yeti ๋ด์ ํ์์์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ ์ ์์ ์ธ ์ธ๋ฑ์ฑ์ ์ ๋ํ ์ ์์๋ค.

