• ABOUT
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

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

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

[๋ฌธ์ œ ์š”์•ฝ] 

  • ์ฆ์ƒ: ๊ฒ€์ƒ‰์—”์ง„ ๋ด‡(Yeti)์ด sitemap ์š”์ฒญ ์‹œ Connection timed out ์˜ค๋ฅ˜ ๋ฐœ์ƒ 
  • ์›์ธ: sitemap์„ ์š”์ฒญ ์‹œ ๊ธฐ์กด ๋ชฉ๋ก API๋ฅผ ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ์ˆœํšŒ ํ˜ธ์ถœํ•˜์—ฌ ์‘๋‹ต ์ง€์—ฐ ๋ฐœ์ƒ 
  • ํ•ด๊ฒฐ: ๊ฐ ๋„๋ฉ”์ธ ๋ณ„๋กœ /sitemap ์—”๋“œ ํฌ์ธํŠธ ์ถ”๊ฐ€ ๋ฐ ISR ์ฃผ๊ธฐ ๋ณ€๊ฒฝ 

 



ํ”„๋ก ํŠธ์—”๋“œ(Next.js) ์„œ๋ฒ„์—์„œ sitemap์„ ์ƒ์„ฑํ•  ๋•Œ ๋ฐฑ์—”๋“œ(Spring Boot) API๋ฅผ ํ†ตํ•ด ๊ธ€ ๋ชฉ๋ก๊ณผ ์นดํ…Œ๊ณ ๋ฆฌ ๋ชฉ๋ก์„ ๋ฐ›์•„์˜ค๋Š” ๊ธฐ์กด ๊ตฌ์กฐ์—์„œ๋Š” ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ํ•œ ๋ฒˆ์— ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๊ณ  ํŽ˜์ด์ง€ ๋‹จ์œ„๋กœ ์ˆœ์ฐจ ์š”์ฒญ์„ ๋ณด๋‚ด๋‹ค ๋ณด๋‹ˆ ๊ธ€ ์ˆ˜๊ฐ€ ๋งŽ์•„์งˆ์ˆ˜๋ก ์‘๋‹ต ์†๋„๊ฐ€ ์ง€์—ฐ๋˜๋Š” ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜์˜€๋‹ค. 

Googlebot์˜ ๊ฒฝ์šฐ ์š”์ฒญ ์‹œ๊ฐ„์ด ๋‹ค์†Œ ๊ธธ๋”๋ผ๋„ ๊ธฐ๋‹ค๋ฆฌ๋Š” ํŽธ์ด์ง€๋งŒ ๋„ค์ด๋ฒ„ ๊ฒ€์ƒ‰์—”์ง„ ๋ด‡(Yeti)์€ ์ผ์ • ์‹œ๊ฐ„ ์ด์ƒ ์‘๋‹ต์ด ์—†์„ ๊ฒฝ์šฐ ์š”์ฒญ์„ ๋Š์–ด๋ฒ„๋ฆฌ๋Š” ๊ฒƒ์œผ๋กœ ํ™•์ธ๋˜์—ˆ๋‹ค. 
 

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 1) sitemap์„ SSG๋กœ ์ƒ์„ฑ 

๊ธฐ์กด SSR ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑํ•˜๋˜ sitemap์„ SSG๋กœ ์ „ํ™˜ํ•˜์—ฌ ๋นŒ๋“œ ์‹œ sitemap ์ƒ์„ฑ 
 

typescript
import { MetadataRoute } from "next";

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


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

ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• 2) ์‹ ๊ทœ ์—”๋“œ ํฌ์ธํŠธ ์ถ”๊ฐ€ 

์ฒ˜์Œ์—๋Š” sitemap ์ƒ์„ฑ์„ ์œ„ํ•ด ๋ณ„๋„์˜ ์ „์šฉ ์ปจํŠธ๋กค๋Ÿฌ๋ฅผ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ๊ณผ๋„ํ•˜๊ฒŒ ๋А๊ปด์ ธ ๊ธฐ์กด ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก API๋ฅผ ๊ทธ๋Œ€๋กœ ํ™œ์šฉํ•˜์˜€๋‹ค. 

๊ทธ๋Ÿฌ๋‚˜ ํŽ˜์ด์ง• ๊ธฐ๋ฐ˜ API์ด๋‹ค ๋ณด๋‹ˆ ์ „์ฒด ๋ฐ์ดํ„ฐ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์—ฌ๋Ÿฌ ๋ฒˆ ํ˜ธ์ถœํ•ด์•ผ ํ–ˆ๊ณ  ๋ณธ๋ฌธ ๊ฐ™์€ sitemap์— ๋ถˆํ•„์š”ํ•œ ํ•„๋“œ๊นŒ์ง€ ํฌํ•จ๋˜์–ด ์˜ค๋ฒ„ํ—ค๋“œ๊ฐ€ ํฌ๊ณ  ๊ธ€ ์ˆ˜๊ฐ€ ๋Š˜์–ด๋‚ ์ˆ˜๋ก ์‘๋‹ต ์†๋„๊ฐ€ ๊ธ‰์ฆํ•˜์˜€๋‹ค. 

๋”ฐ๋ผ์„œ ๊ทผ๋ณธ์ ์ธ ํ•ด๊ฒฐ์„ ์œ„ํ•ด ๊ธฐ์กด ๋„๋ฉ”์ธ์— /sitemap ์—”๋“œ ํฌ์ธํŠธ๋ฅผ ์ถ”๊ฐ€ํ•˜์˜€๋‹ค. 
 

java
@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๋Š” ์•„๋ž˜์™€ ๊ฐ™์ด ์„ค๊ณ„ํ•˜์˜€๋‹ค. 
 

java
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>๋Š” UTC ๊ธฐ๋ฐ˜ ISO 8601 ํฌ๋งท์„ ์š”๊ตฌํ•˜๊ธฐ ๋•Œ๋ฌธ์— DB์—์„œ ์กฐํšŒํ•œ LocalDateTime์„ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ์ƒ์„ฑ์ž๋ฅผ ํ†ตํ•ด Asia/Seoul ํƒ€์ž„์กด์„ ์ง€์ •ํ•œ ๋’ค Instant๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํƒ€์ž„์กด์— ๋…๋ฆฝ์ ์ธ ๊ฐ’์„ ๋ณด๊ด€ํ•˜๋„๋ก ์„ค๊ณ„ํ•˜์˜€๋‹ค. 

์ดํ›„ ์นดํ…Œ๊ณ ๋ฆฌ๋„ ๊ฐ™์€ ๋ฐฉ์‹์œผ๋กœ ์—”๋“œ ํฌ์ธํŠธ๋ฅผ ์„ค๊ณ„ํ•œ ํ›„ ํ”„๋ก ํŠธ์—์„œ Promise.all()์„ ์‚ฌ์šฉํ•˜์—ฌ ๋™์‹œ ํ˜ธ์ถœ์„ ํ†ตํ•ด ์‘๋‹ต ์ง€์—ฐ์„ ์ตœ์†Œํ™”ํ•˜๊ณ  ISR ์ฃผ๊ธฐ๋ฅผ 24์‹œ๊ฐ„์œผ๋กœ ์„ค์ •ํ•˜์˜€๋‹ค. 
 

typescript
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
}


๊ฒฐ๊ณผ์ ์œผ๋กœ sitemap ์‘๋‹ต ์†๋„๋ฅผ 1์ดˆ ์ด๋‚ด๋กœ ์ค„์ด๋Š” ๋™์‹œ์— ๊ฒ€์ƒ‰์—”์ง„์ด ์š”๊ตฌํ•˜๋Š” ํ‘œ์ค€ ํฌ๋งท์„ ์ง€ํ‚ฌ ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ๋‹ค. 
 

์‚ฌ์ดํŠธ๋งต ์˜ค๋ฅ˜ ํ•ด๊ฒฐ

 

์ด์ „ ๊ธ€
โšก๏ธ React์—์„œ ์„œ๋ฒ„ ์ƒํƒœ ๊ด€๋ฆฌํ•˜๊ธฐ: useQuery ํ™œ์šฉ๋ฒ•
๋‹ค์Œ ๊ธ€
๐Ÿ”‘ ํ•ด์‹œ ํ…Œ์ด๋ธ”(Hash Table) ๊ฐœ๋…๊ณผ ์ถฉ๋Œ ์ฒ˜๋ฆฌ ๋ฐฉ์‹ ์ •๋ฆฌ
์žฅ์‹์šฉ ๋กœ๊ณ