• ABOUT
  • PORTFOLIO
  • POSTS
  • GUESTBOOK

ยฉ 2025-2026 BlueCool12 All rights reserved.

2025.09.11ํŠธ๋Ÿฌ๋ธ”์ŠˆํŒ…

๐Ÿž ๊ฒ€์ƒ‰์—”์ง„ ํƒ€์ž„์•„์›ƒ ๋ฌธ์ œ ํ•ด๊ฒฐ - Sitemap ์„ฑ๋Šฅ ์ตœ์ ํ™”

1. ๋ฌธ์ œ ์ƒํ™ฉ

๊ธฐ์กด ํ”„๋ก ํŠธ์—”๋“œ(Next.js) ์„œ๋ฒ„์˜ ์‚ฌ์ดํŠธ๋งต ์ƒ์„ฑ ๋กœ์ง์€ ๋ฐฑ์—”๋“œ์˜ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก API๋ฅผ ํ™œ์šฉํ•˜๊ณ  ์žˆ์—ˆ๋‹ค. ์ด ๊ณผ์ •์—์„œ ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ–ˆ๋‹ค.

  • ์‘๋‹ต ์ง€์—ฐ ๋ฐ ํƒ€์ž„์•„์›ƒ: ํŽ˜์ด์ง• ๊ธฐ๋ฐ˜ API๋ฅผ ์‚ฌ์šฉํ•จ์— ๋”ฐ๋ผ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์œ„ํ•ด ์ˆœ์ฐจ์ ์ธ HTTP ์š”์ฒญ์ด ๋ฐœ์ƒํ–ˆ๊ณ  ๋ฐ์ดํ„ฐ ์–‘์— ๋น„๋ก€ํ•ด ์‘๋‹ต ์‹œ๊ฐ„์ด ์ฆ๊ฐ€ํ•จ
  • ๊ฒ€์ƒ‰์—”์ง„ ํฌ๋กค๋ง ์‹คํŒจ: Googlebot๊ณผ ๋‹ฌ๋ฆฌ ๋„ค์ด๋ฒ„์˜ Yeti ๋ด‡์€ ์‘๋‹ต ๋Œ€๊ธฐ ์‹œ๊ฐ„์ด ์ผ์ • ์ˆ˜์ค€์„ ์ดˆ๊ณผํ•  ๊ฒฝ์šฐ ์—ฐ๊ฒฐ์„ ๊ฐ•์ œ๋กœ ์ข…๋ฃŒํ•˜์—ฌ ์‚ฌ์ดํŠธ๋งต ์ˆ˜์ง‘์— ์‹คํŒจํ•˜๋Š” ํ˜„์ƒ ํ™•์ธ
  • ๋ฆฌ์†Œ์Šค ๋‚ญ๋น„: ์‚ฌ์ดํŠธ๋งต ์ƒ์„ฑ์— ๋ถˆํ•„์š”ํ•œ ๋ณธ๋ฌธ ๋‚ด์šฉ ๋“ฑ์˜ ํ•„๋“œ๊นŒ์ง€ ํ•จ๊ป˜ ์กฐํšŒ๋˜์–ด ๋„คํŠธ์›Œํฌ ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ๋ฐœ์ƒ


2. ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1) SSG ๋ฐฉ์‹ ์ „ํ™˜

export const dynamic = 'force-static'; ์„ค์ •์„ ํ†ตํ•ด ๋นŒ๋“œ ์‹œ์ ์— ์‚ฌ์ดํŠธ๋งต์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜์—ฌ ์‘๋‹ต ์†๋„๋ฅผ ์ตœ์ ํ™”ํ–ˆ๋‹ค.

import { MetadataRoute } from "next";

// export const dynamic = 'force-dynamic'; ๊ธฐ์กด SSR ์„ค์ •
export const dynamic = 'force-static'; // SSG ๋ฐฉ์‹์œผ๋กœ ์ „ํ™˜

์ด ๋ฐฉ๋ฒ•์œผ๋กœ ์‘๋‹ต์†๋„๋Š” ํฌ๊ฒŒ ์ค„์ผ ์ˆ˜ ์žˆ์—ˆ์ง€๋งŒ ์ƒˆ๋กœ์šด ์ฝ˜ํ…์ธ ๊ฐ€ ์ถ”๊ฐ€๋  ๋•Œ๋งˆ๋‹ค ์ „์ฒด ํ”„๋กœ์ ํŠธ๋ฅผ ์žฌ๋นŒ๋“œํ•ด์•ผ ํ•˜๋ฏ€๋กœ ์‹ค์‹œ๊ฐ„ ๋ฐ˜์˜ ์ธก๋ฉด์—์„œ ์œ ์—ฐ์„ฑ์ด ๋–จ์–ด์กŒ๋‹ค.


2) ์‹ ๊ทœ ์—”๋“œ ํฌ์ธํŠธ ์ถ”๊ฐ€ ๋ฐ ISR ์ ์šฉ

์ฒ˜์Œ์—๋Š” 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 ๋ด‡์˜ ํƒ€์ž„์•„์›ƒ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  ์ •์ƒ์ ์ธ ์ธ๋ฑ์‹ฑ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์—ˆ๋‹ค.


แ„‰แ…กแ„‹แ…ตแ„แ…ณแ„†แ…ขแ†ธ แ„‹แ…ฉแ„…แ…ฒ แ„’แ…ขแ„€แ…งแ†ฏ.webp

ๅ‰ใฎ่จ˜ไบ‹
โšก๏ธ React์—์„œ ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌํ•˜๊ธฐ: useQuery ํ™œ์šฉ๋ฒ•
ๆฌกใฎ่จ˜ไบ‹
๐Ÿ”‘ ํ•ด์‹œ ํ…Œ์ด๋ธ”(Hash Table) ๊ฐœ๋…๊ณผ ์ถฉ๋Œ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ •๋ฆฌ
่ฃ…้ฃพใƒญใ‚ด