• 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

Previous post
⚑️ Reactμ—μ„œ μ„œλ²„ μƒνƒœ κ΄€λ¦¬ν•˜κΈ°: useQuery ν™œμš©λ²•
Next post
πŸ”‘ ν•΄μ‹œ ν…Œμ΄λΈ”(Hash Table) κ°œλ…κ³Ό 좩돌 처리 방식 정리
Decorative logo