• ABOUT
  • PORTFOLIO
  • POSTS
  • GUESTBOOK

ยฉ 2025-2026 BlueCool12 All rights reserved.

2025.07.01Next.js

๐Ÿ”Ž Next.js๋ฅผ ์ด์šฉํ•œ SEO ์ตœ์ ํ™”

SEO(Search Engine Optimization)๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ํŠธ๋ž˜ํ”ฝ์„ ์ขŒ์šฐํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ๋‹ค. React์˜ ๊ธฐ๋ณธ ๋ Œ๋”๋ง ๋ฐฉ์‹์ธ CSR(Client-Side Rendering)๋งŒ์œผ๋กœ๋Š” ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ์—๊ฒŒ ์ œ๋Œ€๋กœ ๋…ธ์ถœํ•˜๊ธฐ ์–ด๋ ต๋‹ค.

Next.js๋ฅผ ํ™œ์šฉํ•˜๋ฉด ํฌ๋กค๋Ÿฌ๊ฐ€ ์ฝ˜ํ…์ธ ๋ฅผ ๋น ์ง์—†์ด ์ˆ˜์ง‘ํ•˜๊ณ , ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—๋„ ์ •ํ™•ํžˆ ๋…ธ์ถœ๋˜๋Š” ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค.

1. ๋ Œ๋”๋ง ์ „๋žต ์ตœ์ ํ™”

๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ(Googlebot, Yeti ๋“ฑ)๋Š” ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๋ฉด ๋จผ์ € HTML ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ๋ถ„์„ํ•œ๋‹ค. CSR ๋ฐฉ์‹์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋œ ํ›„์—์•ผ ํ™”๋ฉด์ด ์™„์„ฑ๋˜๋ฏ€๋กœ ์ดˆ๊ธฐ HTML์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. ์ด ๊ฒฝ์šฐ ํฌ๋กค๋Ÿฌ๊ฐ€ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๋Œ€๋กœ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜ ์ธ๋ฑ์‹ฑ์ด ์ง€์—ฐ๋  ์ˆ˜ ์žˆ๋‹ค.

๋ฐ˜๋ฉด ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ์™„์„ฑ๋œ HTML์„ ์ œ๊ณตํ•˜๋ฉด ํฌ๋กค๋Ÿฌ๊ฐ€ ์ฆ‰์‹œ ์ฝ˜ํ…์ธ ๋ฅผ ์ˆ˜์ง‘ํ•  ์ˆ˜ ์žˆ์–ด SEO์— ์œ ๋ฆฌํ•˜๋‹ค. Next.js๋Š” ์ƒํ™ฉ์— ๋งž๊ฒŒ ์„ ํƒํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ ๋ Œ๋”๋ง ์ „๋žต์„ ์ง€์›ํ•œ๋‹ค.

SSR (Server-Side Rendering)

SSR์€ ์‚ฌ์šฉ์ž์˜ ํŽ˜์ด์ง€ ์š”์ฒญ์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ƒˆ๋กœ ๊ฐ€์ ธ์™€ HTML์„ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ Œ๋”๋ง ํ•ด ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค.

SSR์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

[์บ์‹ฑ ๋น„ํ™œ์„ฑํ™”๋ฅผ ํ†ตํ•œ ๋™์  ๋ Œ๋”๋ง ์œ ๋„]
fetch ์š”์ฒญ ์‹œ ์บ์‹ฑ ํ•˜์ง€ ์•Š๋„๋ก ์˜ต์…˜์„ ์„ค์ •ํ•˜๋ฉด ํ•ด๋‹น ํŽ˜์ด์ง€๋Š” ๋งค ์š”์ฒญ๋งˆ๋‹ค ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง ๋œ๋‹ค. Next.js 15๋ถ€ํ„ฐ๋Š” fetch์˜ ๊ธฐ๋ณธ๊ฐ’์ด 'no-store'์ด๋ฏ€๋กœ ๋ณ„๋„ ์˜ต์…˜ ์—†์ด๋„ SSR๋กœ ๋™์ž‘ํ•˜์ง€๋งŒ, ๋ช…์‹œ์ ์œผ๋กœ ์ž‘์„ฑํ•˜๋ฉด ์ฝ”๋“œ์˜ ์˜๋„๋ฅผ ํŒŒ์•…ํ•˜๊ธฐ ์‰ฝ๋‹ค.

export default async function Page() {
const res = await fetch('https://api.pyomin.com/posts', {
cache: 'no-store',
});
if (!res.ok) throw new Error('Failed to fetch data');

const data = await res.json();
return <main>{data.title}</main>
}


[๋™์  ํ•จ์ˆ˜ ์‚ฌ์šฉ]
cookies(), headers(), searchParams์ฒ˜๋Ÿผ ์š”์ฒญ ์‹œ์ ์—๋งŒ ์•Œ ์ˆ˜ ์žˆ๋Š” ์ •๋ณด์— ์ ‘๊ทผํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ปดํฌ๋„ŒํŠธ ๋‚ด์—์„œ ํ˜ธ์ถœํ•˜๋ฉด Next.js๋Š” ํ•ด๋‹น ๋ผ์šฐํŠธ๋ฅผ ์ž๋™์œผ๋กœ ๋™์  ๋ Œ๋”๋ง ํŽ˜์ด์ง€๋กœ ์ „ํ™˜ํ•œ๋‹ค.

import { cookies } from 'next/headers';

export default function Page() {
const cookieStore = cookies();
const theme = cookieStore.get('theme');

return <main>Current Theme: {theme?.value}</main>;
}โ€‹


[๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ์„ค์ • ๊ฐ•์ œ]
๋ผ์šฐํŠธ ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— ๋ Œ๋”๋ง ๋ฐฉ์‹์„ ์„ ์–ธํ•˜๋ฉด ๋‚ด๋ถ€ ๊ตฌํ˜„๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ด๋‹น ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ SSR๋กœ ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

export const dynamic = 'force-dynamic';


SSG (Static Site Generation)

SSG๋Š” ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ์„œ๋ฒ„์— ๋ฐฐํฌํ•˜๊ธฐ ์ „ ๋นŒ๋“œ ์‹œ์ ์— ๊ฐ ํŽ˜์ด์ง€์˜ HTML์„ ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•ด ๋‘๋Š” ๋ Œ๋”๋ง ๋ฐฉ์‹์ด๋‹ค. ์‚ฌ์šฉ์ž๊ฐ€ ํŽ˜์ด์ง€๋ฅผ ์š”์ฒญํ•˜๋ฉด ์„œ๋ฒ„๋Š” ์ด๋ฏธ ๋งŒ๋“ค์–ด์ง„ ์ •์  HTML ํŒŒ์ผ์„ ์ฆ‰์‹œ ๋ฐ˜ํ™˜ํ•œ๋‹ค.

๋ฐ์ดํ„ฐ ํŒจ์นญ ์œ ๋ฌด์™€ ๋ผ์šฐํŠธ ํ˜•ํƒœ์— ๋”ฐ๋ผ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

[๋ฐ์ดํ„ฐ ํŒจ์นญ์ด ์—†๋Š” ํŽ˜์ด์ง€]
fetch ํ•จ์ˆ˜๋‚˜ ๋™์  ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ˆœ์ˆ˜ UI ์ปดํฌ๋„ŒํŠธ๋Š” ๋ณ„๋„ ์„ค์ • ์—†์ด๋„ ๋นŒ๋“œ ์‹œ์ ์— ์ •์  HTML๋กœ ์ƒ์„ฑ๋œ๋‹ค.


[๋ฐ์ดํ„ฐ ํŒจ์นญ์ด ํฌํ•จ๋œ ํŽ˜์ด์ง€]
API ๋“ฑ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ํ˜ธ์ถœํ•ด ํŽ˜์ด์ง€๋ฅผ ๊ตฌ์„ฑํ•  ๋•Œ ๋นŒ๋“œ ์‹œ์  ๋ฐ์ดํ„ฐ๋ฅผ ์ •์  ํŽ˜์ด์ง€๋กœ ๊ณ ์ •ํ•˜๋ ค๋ฉด cache: 'force-cache'๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•œ๋‹ค.

interface PageData {
title: string;
}โ€‹

export default async function Page() {
const res = await fetch('https://api.pyomin.com/data', {
cache: 'force-cache',
});
if (!res.ok) throw new Error('Failed to fetch data');

const data: PageData = await res.json();
return <main>{data.title}</main>;
}


[๋™์  ๋ผ์šฐํŠธ ํŽ˜์ด์ง€]
[id], [slug] ๋“ฑ URL ๊ฒฝ๋กœ๊ฐ€ ๋™์ ์ธ ํŽ˜์ด์ง€๋ฅผ ๋นŒ๋“œ ์‹œ์ ์— ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•˜๋ ค๋ฉด generateStaticParams ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐฐ์—ด์„ ๋ฐ”ํƒ•์œผ๋กœ Next.js๊ฐ€ ๊ฐ ์ •์  HTML ํŒŒ์ผ์„ ๋นŒ๋“œ ํ•œ๋‹ค.

interface Post {
slug: string;
}โ€‹

export async function generateStaticParams() {
const res = await fetch('https://api.pyomin.com/posts');
if (!res.ok) throw new Error('Failed to fetch posts');

const posts: Post[] = await res.json();
return posts.map((post) => ({
slug: String(post.slug),
}));
}


[๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ์„ค์ • ๊ฐ•์ œ]
๋ผ์šฐํŠธ ํŒŒ์ผ(page.tsx ๋˜๋Š” layout.tsx) ์ตœ์ƒ๋‹จ์— ๋ Œ๋”๋ง ๋ฐฉ์‹์„ ์„ ์–ธํ•˜๋ฉด ํŒŒ์ผ ๋‚ด๋ถ€ ๋กœ์ง๊ณผ ๋ฌด๊ด€ํ•˜๊ฒŒ ํ•ด๋‹น ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ SSG๋กœ ๊ฐ•์ œํ•  ์ˆ˜ ์žˆ๋‹ค.

export const dynamic = 'force-static';


ISR (Incremental Static Regeneration)

ISR์€ ์ „์ฒด ์‚ฌ์ดํŠธ๋ฅผ ๋‹ค์‹œ ๋นŒ๋“œ ํ•˜์ง€ ์•Š๊ณ ๋„ ํŠน์ • ์ฃผ๊ธฐ๋‚˜ ์กฐ๊ฑด์— ๋”ฐ๋ผ ๊ธฐ์กด์˜ ์ •์  ํŽ˜์ด์ง€(SSG)๋ฅผ ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๋ Œ๋”๋ง ๋ฐฉ์‹์ด๋‹ค. ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” ํ•ญ์ƒ ์บ์‹œ ๋œ ์ •์  ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•˜๋ฉด์„œ๋„ ๋ฐ์ดํ„ฐ์˜ ์ตœ์‹ ์„ฑ์„ ์œ ์ง€ํ•  ์ˆ˜ ์žˆ๋‹ค.

ISR์€ ๋‹ค์Œ๊ณผ ๊ฐ™์ด ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋‹ค.

[์‹œ๊ฐ„ ๊ธฐ๋ฐ˜ ์žฌ๊ฒ€์ฆ]
fetch ์š”์ฒญ ์‹œ next.revalidate ์˜ต์…˜์œผ๋กœ ์ผ์ • ์‹œ๊ฐ„(์ดˆ)์„ ์ง€์ •ํ•˜๋ฉด ํ•ด๋‹น ์ฃผ๊ธฐ๊ฐ€ ์ง€๋‚  ๋•Œ๋งˆ๋‹ค ๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€์ ธ์™€ ์บ์‹œ๋ฅผ ๊ฐฑ์‹ ํ•œ๋‹ค.

interface PageData {
title: string;
}

export default async function Page() {
const res = await fetch('https://api.pyomin.com/posts', {
next: { revalidate: 60 },
});
if (!res.ok) throw new Error('Failed to fetch data');

const data: PageData = await res.json();
return <main>{data.title}</main>;
}


[์˜จ๋””๋งจ๋“œ ์žฌ๊ฒ€์ฆ]
์‹œ๊ฐ„ ์ฃผ๊ธฐ์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ์ฆ‰์‹œ ์บ์‹œ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋ฌดํšจํ™”ํ•˜๋Š” ๋ฐฉ์‹์ด๋‹ค. fetch ์š”์ฒญ์— ํƒœ๊ทธ๋ฅผ ์ง€์ •ํ•œ ๋’ค, ๋ฐ์ดํ„ฐ๊ฐ€ ์—…๋ฐ์ดํŠธ๋˜๋Š” ์‹œ์ ์— ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ ๋“ฑ์—์„œ revalidateTag ๋˜๋Š” revalidatePath๋ฅผ ํ˜ธ์ถœํ•œ๋‹ค.

// 1. fetch ์š”์ฒญ์— ํƒœ๊ทธ ๋ถ€์—ฌ
const res = await fetch('https://api.pyomin.com/posts', {
next: { tags: ['posts'] },
});
// 2. ๋ฐ์ดํ„ฐ ์ˆ˜์ • ์‹œ ํƒœ๊ทธ ๊ธฐ๋ฐ˜์œผ๋กœ ์บ์‹œ ๋ฌดํšจํ™”
import { revalidateTag } from 'next/cache';

export async function POST(request: Request) {
// DB ์—…๋ฐ์ดํŠธ ๋กœ์ง ์ดํ›„...
// 'posts' ํƒœ๊ทธ๊ฐ€ ๋ถ™์€ ๋ชจ๋“  fetch ์บ์‹œ๋ฅผ ์ฆ‰์‹œ ์‚ญ์ œํ•˜๊ณ  ์žฌ์ƒ์„ฑ
revalidateTag('posts')โ€‹;
return Response.json({ revalidated: true });
}


[๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ์„ค์ • ๊ฐ•์ œ]
๋ผ์šฐํŠธ ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— revalidate ๋ณ€์ˆ˜๋ฅผ ์„ ์–ธํ•˜๋ฉด ํŒŒ์ผ ๋‚ด๋ถ€์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ ํŒจ์นญ๊ณผ ๋ Œ๋”๋ง์˜ ์žฌ๊ฒ€์ฆ ์ฃผ๊ธฐ๋ฅผ ์ผ๊ด„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

// ํ•ด๋‹น ๋ผ์šฐํŠธ์˜ ๋ชจ๋“  ๋ Œ๋”๋ง ๊ฒฐ๊ณผ๋ฅผ 3600์ดˆ(1์‹œ๊ฐ„)๋งˆ๋‹ค ์žฌ๊ฒ€์ฆ
export const revalidate = 3600;

ํŽ˜์ด์ง€ ๋‹จ์œ„์˜ ๋ผ์šฐํŠธ ์„ธ๊ทธ๋จผํŠธ ์„ค์ •๊ณผ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€ fetch ํ•จ์ˆ˜์˜ ๊ฐœ๋ณ„ ์บ์‹œ ์„ค์ •์ด ํ•จ๊ป˜ ์žˆ์œผ๋ฉด ๋” ์งง์€ ์žฌ๊ฒ€์ฆ ์ฃผ๊ธฐ๋ฅผ ๋ผ์šฐํŠธ ์ „์ฒด์˜ ๊ธฐ์ค€์œผ๋กœ ๋ณ‘ํ•ฉํ•ด ์ ์šฉํ•œ๋‹ค.


2. ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ (Metadata API)

Next.js๋Š” ๊ธฐ์กด next/head ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋Œ€์ฒดํ•˜๋Š” Metadata API๋ฅผ ์ œ๊ณตํ•œ๋‹ค. title, description, Open Graph ์ •๋ณด ๋“ฑ <head> ์˜์—ญ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง ํ•ด, ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ๊ฐ€ ํŽ˜์ด์ง€ ์ •๋ณด๋ฅผ ๋ช…ํ™•ํ•˜๊ฒŒ ์ˆ˜์ง‘ํ•˜๋„๋ก ํ•œ๋‹ค.

์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

ํŽ˜์ด์ง€์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ๊ณ ์ •๋ผ ์žˆ์„ ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค. layout.tsx ๋˜๋Š” page.tsx ํŒŒ์ผ ์ตœ์ƒ๋‹จ์—์„œ metadata ๊ฐ์ฒด๋ฅผ export ํ•˜๋ฉด, Next.js๊ฐ€ ๋นŒ๋“œ ์‹œ์ ์ด๋‚˜ ๋ Œ๋”๋ง ์‹œ์ ์— ์ด ๊ฐ์ฒด๋ฅผ ์ฝ์–ด <head> ํƒœ๊ทธ ์•ˆ์— ๋ฉ”ํƒ€ ํƒœ๊ทธ๋กœ ์ฃผ์ž…ํ•œ๋‹ค.

import type { Metadata } from 'next';

export const metadata: Metadata = {
title: 'About',
description: 'BlueCool12์˜ ์ด๋ ฅ๊ณผ ํ”„๋กœ์ ํŠธ๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.',
alternates: {
canonical: '/about',
},
openGraph: {
title: 'About',
description: 'BlueCool12์˜ ์ด๋ ฅ๊ณผ ํ”„๋กœ์ ํŠธ๋ฅผ ์†Œ๊ฐœํ•ฉ๋‹ˆ๋‹ค.',
url: 'https://pyomin.com/about',
},
};

export default function AboutPage() {
// ...
}โ€‹


๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

[id], [slug]์™€ ๊ฐ™์€ ๋™์  ๋ผ์šฐํŠธ์—์„œ ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ์ด๋‚˜ ์„ค๋ช…์ฒ˜๋Ÿผ ํŽ˜์ด์ง€ ๋ฐ์ดํ„ฐ์— ๋”ฐ๋ผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ”๊ฟ”์•ผ ํ•  ๋•Œ ์‚ฌ์šฉํ•œ๋‹ค.

ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์™€ ๋™์ผํ•˜๊ฒŒ params์™€ searchParams๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๋‚ด๋ถ€์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์กฐํšŒํ•œ ํ›„, generateMetadata ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ์ฒด๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•œ๋‹ค.

import type { Metadata }โ€‹ from 'next';

interface Props {
params: Promise<{ slug: string }>;
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug: rawSlug } = await params;
const slug = decodeURIComponent(rawSlug);
const post = await postService.getPostBySlug(slug);

return {
title: post.title,
description: post.description,
alternates: {
canonical: `/posts/${slug}`,
},
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: `https://pyomin.com/posts/${slug}`,
},
};
}โ€‹

generateMetadata ๋‚ด๋ถ€์˜ fetch ์š”์ฒญ๊ณผ ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋™์ผํ•œ API๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ๊ฒฝ์šฐ Next.js๊ฐ€ ์ž๋™์œผ๋กœ ์š”์ฒญ์„ ๋ฉ”๋ชจ์ด์ œ์ด์…˜ํ•˜์—ฌ ์‹ค์ œ ๋„คํŠธ์›Œํฌ ์š”์ฒญ์€ ํ•œ ๋ฒˆ๋งŒ ์ˆ˜ํ–‰๋œ๋‹ค.


๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…œํ”Œ๋ฆฟ

ํŽ˜์ด์ง€ ์ œ๋ชฉ์— ๊ณตํ†ต ์ ‘๋ฏธ์‚ฌ๋‚˜ ์ ‘๋‘์‚ฌ๋ฅผ ์ž๋™์œผ๋กœ ๋ถ™์ด๊ณ  ์‹ถ์„ ๋•Œ๋Š” layout.tsx์™€ ๊ฐ™์€ ์ƒ์œ„ ๋ ˆ์ด์•„์›ƒ ํŒŒ์ผ์—์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ…œํ”Œ๋ฆฟ์„ ์„ค์ •ํ•œ๋‹ค.

import type { Metadata } from 'next';

export const metadata: Metadata = {
title: {
template: '%s | BLUECOOL',
default: 'BLUECOOL',
},
description: 'BlueCool์€ ๋‹ค์–‘ํ•œ ์ธ์‚ฌ์ดํŠธ๋ฅผ ์ „ํ•˜๋Š” ๊ฐœ๋ฐœ์ž ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.',
};

๊ฒฐ๊ณผ์ ์œผ๋กœ ํ•˜์œ„ ํŽ˜์ด์ง€์—์„œ๋Š” title: 'About' ์œผ๋กœ๋งŒ ์„ค์ •ํ•ด๋„ ์‹ค์ œ ๋ธŒ๋ผ์šฐ์ €์™€ ๊ฒ€์ƒ‰ ์—”์ง„์—๋Š” <title>About | BLUECOOL</title>๋กœ ๋…ธ์ถœ๋œ๋‹ค.


ํŒŒ์ผ ๊ธฐ๋ฐ˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

favicon, opengraph-image์ฒ˜๋Ÿผ ์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํŒŒ์ผ์€ ์ฝ”๋“œ๋กœ ์ž‘์„ฑํ•˜์ง€ ์•Š๊ณ  ์ •ํ•ด์ง„ ์ด๋ฆ„ ๊ทœ์น™์— ๋”ฐ๋ผ ์ง€์ •๋œ ํด๋” ๊ฒฝ๋กœ์— ๋ฐฐ์น˜ํ•˜๋Š” ๊ฒƒ๋งŒ์œผ๋กœ ์ž๋™์œผ๋กœ ๋ฉ”ํƒ€ ํƒœ๊ทธ๋กœ ๋ณ€ํ™˜๋œ๋‹ค.

์ด ๋ฐฉ์‹์€ ์ฝ”๋“œ ๋ณต์žก๋„๋ฅผ ์ค„์ด๊ณ , ์‚ฌ์ดํŠธ ์•„์ด์ฝ˜์ด๋‚˜ Open Graph ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ๋ณ€๊ฒฝ ๋นˆ๋„๊ฐ€ ๋‚ฎ์€ ์ •์  ๋ฆฌ์†Œ์Šค๋ฅผ ๊ด€๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•˜๋‹ค.


3. ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋ง ๋ฐ ์ธ๋ฑ์‹ฑ ์„ค์ •

๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ๋Š” robots.txt๋ฅผ ์ฝ์–ด ํฌ๋กค๋ง ํ—ˆ์šฉ ๋ฒ”์œ„๋ฅผ ํŒŒ์•…ํ•˜๊ณ , sitemap.xml์„ ํ†ตํ•ด ์ „์ฒด ํŽ˜์ด์ง€ ๊ตฌ์กฐ์™€ ์ตœ์‹  ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ์„ ์ˆ˜์ง‘ํ•œ๋‹ค. Next.js์—์„œ๋Š” ์ด ๋‘ ํŒŒ์ผ์„ ์ •์ ์œผ๋กœ ์ง์ ‘ ๋ฐฐ์น˜ํ•˜๊ฑฐ๋‚˜ ์ฝ”๋“œ๋กœ ๋™์  ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค.

robots.txt

app/robots.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด MetadataRoute.Robots ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•˜์—ฌ, ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ๋กœ์™€ ์ฐจ๋‹จํ•  ๊ฒฝ๋กœ๋ฅผ ์ •์˜ํ•œ๋‹ค.

import { MetadataRoute } from 'next';โ€‹

export default function robots(): MetadataRoute.Robots {
return {
rules: [{ userAgent: '*', allow: '/' }],
sitemap: 'https://pyomin.com/sitemap.xml',
};
}

๋™์  ๋ฐฉ์‹์„ ์“ฐ๋ฉด ํ™˜๊ฒฝ ๋ณ€์ˆ˜์— ๋”ฐ๋ผ ํฌ๋กค๋ง ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

sitemap.xml

app/sitemap.ts ํŒŒ์ผ์„ ์ƒ์„ฑํ•ด MetadataRoute.Sitemap ๋ฐฐ์—ด์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋ฅผ ์ž‘์„ฑํ•œ๋‹ค.

์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ๋‚ด ๋ชจ๋“  ์œ ํšจํ•œ URL ๋ชฉ๋ก๊ณผ ๊ฐ ํŽ˜์ด์ง€์˜ ๋งˆ์ง€๋ง‰ ์ˆ˜์ •์ผ, ๋ณ€๊ฒฝ ๋นˆ๋„, ์ค‘์š”๋„๋ฅผ ๊ฒ€์ƒ‰ ์—”์ง„์— ์ œ๊ณตํ•ด ์ธ๋ฑ์‹ฑ ํšจ์œจ์„ ๋†’์ผ ์ˆ˜ ์žˆ๋‹ค.

import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://api.pyomin.com';

// ๋™์  ๊ฒฝ๋กœ
const posts = await fetch(`${baseUrl}/posts`).then((res) => res.json());
const postUrls: MetadataRoute.Sitemap = posts.map((post) => ({
url: `https://pyomin.com/posts/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly',
priority: 0.8,
}));

// ์ •์  ๊ฒฝ๋กœ
const staticUrls: MetadataRoute.Sitemap = [
{
url: 'https://pyomin.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1.0,
},
{
url: 'https://pyomin.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.5,
},
];

// ์ •์  ๊ฒฝ๋กœ์™€ ๋™์  ๊ฒฝ๋กœ ๋ณ‘ํ•ฉํ•˜์—ฌ ๋ฐ˜ํ™˜
return [...staticUrls, ...postUrls];
}โ€‹


4. ์ด๋ฏธ์ง€ ์ตœ์ ํ™”

๊ฒ€์ƒ‰ ์—”์ง„์€ ํŽ˜์ด์ง€ ๊ฒฝํ—˜์„ ํ‰๊ฐ€ํ•  ๋•Œ ์ฝ”์–ด ์›น ๋ฐ”์ดํƒˆ ์ง€ํ‘œ๋ฅผ ๊ฒ€์ƒ‰ ๋žญํ‚น์— ๋ฐ˜์˜ํ•œ๋‹ค. Next.js์˜ Image ์ปดํฌ๋„ŒํŠธ๋Š” ์ด ์ค‘์—์„œ๋„ LCP(์ตœ๋Œ€ ์ฝ˜ํ…์ธ  ํ’€ ํŽ˜์ธํŠธ)์™€ CLS(๋ˆ„์  ๋ ˆ์ด์•„์›ƒ ์ด๋™)๋ฅผ ์‹œ์Šคํ…œ ๋ ˆ๋ฒจ์—์„œ ์ตœ์ ํ™”ํ•˜๋„๋ก ์„ค๊ณ„๋˜์–ด ์žˆ๋‹ค.

Image ์ปดํฌ๋„ŒํŠธ๋Š” width์™€ height ์†์„ฑ์„ ํ•„์ˆ˜๋กœ ์š”๊ตฌํ•ด ๋ Œ๋”๋ง ์‹œ์ ์— ์ด๋ฏธ์ง€ ์˜์—ญ์„ ๋ฏธ๋ฆฌ ํ™•๋ณดํ•œ๋‹ค. ์ด๋ฅผ ํ†ตํ•ด ๋ ˆ์ด์•„์›ƒ ์ด๋™์„ ์›์ฒœ์ ์œผ๋กœ ๋ฐฉ์ง€ํ•˜๋ฉฐ ์ง€์—ฐ ๋กœ๋”ฉ ์ ์šฉ, WebP/AVIF ๊ฐ™์€ ํ˜„๋Œ€์  ์ด๋ฏธ์ง€ ํฌ๋งท์œผ๋กœ ์ž๋™ ๋ณ€ํ™˜ํ•ด ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•œ๋‹ค.

import Image from 'next/image';

export default function AboutPage() {
return (
<main>
<Image
src='/images/about.webp'
alt='BlueCool12 ๋งˆ์Šค์ฝ”ํŠธ ์ด๋ฏธ์ง€'
width={160}
height={160}
priority // ๋ธŒ๋ผ์šฐ์ €์— ์ตœ์šฐ์„  ๋กœ๋“œ ์ง€์‹œ (LCP ์ตœ์ ํ™”)
/>โ€‹
</main>
);
}

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

๋˜ํ•œ alt ์†์„ฑ์€ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์ ‘๊ทผ์„ฑ ์š”์†Œ์ด์ž, ๊ฒ€์ƒ‰ ์—”์ง„์ด ์ด๋ฏธ์ง€์˜ ์˜๋ฏธ์™€ ๋งฅ๋ฝ์„ ์ดํ•ดํ•˜๋Š” ๋ฐ๋„ ์“ฐ์ด๋ฏ€๋กœ ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒŒ ์ข‹๋‹ค.


5. JSON-LD (๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ)

JSON-LD๋Š” ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ๊ฐ€ ์›น ํŽ˜์ด์ง€์˜ ๋ฌธ๋งฅ๊ณผ ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๊ณ„์ ์œผ๋กœ ์ดํ•ดํ•˜๋„๋ก ๋•๋Š” ํ‘œ์ค€ ํฌ๋งท์ด๋‹ค.

์ผ๋ฐ˜์ ์ธ ํ…์ŠคํŠธ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์™ธ์—๋„ ๋ณ„์ , ๊ฐ€๊ฒฉ, ๋ฆฌ๋ทฐ, FAQ ๋“ฑ์˜ ์ •๋ณด๋ฅผ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํŽ˜์ด์ง€์— ์‹œ๊ฐ์ ์œผ๋กœ ๋…ธ์ถœํ•˜์—ฌ ์‚ฌ์šฉ์ž ํด๋ฆญ๋ฅ  ํ–ฅ์ƒ์— ๊ธ์ •์ ์ธ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋‹ค. ๊ตฌ๊ธ€์€ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•  ๋•Œ JSON-LD ํ˜•์‹์„ ๊ณต์‹์ ์œผ๋กœ ๊ถŒ์žฅํ•œ๋‹ค.

// app/posts/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;

// ์˜ˆ์‹œ ๋ฐ์ดํ„ฐ
const post = {
title: 'Next.js๋ฅผ ์ด์šฉํ•œ SEO ์ตœ์ ํ™”',
authorName: 'BLUECOOL',
publishedAt: '2025-07-01T12:00:00+09:00',
description: 'Next.js์—์„œ์˜ SEO ์ตœ์ ํ™” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด…๋‹ˆ๋‹ค.',
url: `https://pyomin.com/posts/${slug}`,
};

// JSON-LD ๊ฐ์ฒด ์ƒ์„ฑ (schema.org ํ‘œ์ค€ ๊ทœ๊ฒฉ ์ค€์ˆ˜)
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
author: {
'@type': 'Person',
name: post.authorName,
url: 'https://pyomin.com/about'
},
datePublished: post.publishedAt,
description: post.description,
url: post.url
};

return (
<section>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(jsonLd),
}}
/>

<article>
<h1>{post.title}</h1>
<p>{post.description}</p>
</article>
</section>
);
}

JSON-LD์— ํฌํ•จ๋œ ์ •๋ณด๋Š” ์‹ค์ œ ํŽ˜์ด์ง€ ํ™”๋ฉด์— ๋…ธ์ถœ๋˜๋Š” ์ฝ˜ํ…์ธ ์™€ ์ผ์น˜ํ•ด์•ผ ํ•œ๋‹ค. ํ™”๋ฉด์— ๋ณด์ด์ง€ ์•Š๋Š” ์ˆจ๊ฒจ์ง„ ๋ฐ์ดํ„ฐ๋งŒ ๊ตฌ์กฐํ™”๋œ ๋ฐ์ดํ„ฐ๋กœ ์ œ๊ณตํ•˜๋ฉด ๊ฒ€์ƒ‰ ์—”์ง„ ์ŠคํŒธ ์ •์ฑ… ์œ„๋ฐ˜์œผ๋กœ ๊ฐ„์ฃผ๋  ์ˆ˜ ์žˆ๋‹ค.

Previous post
๐Ÿ‘พ ๋ฆฌ๋ˆ…์Šค ๊ธฐ๋ณธ ๋ช…๋ น์–ด ์ •๋ฆฌ
Next post
๐Ÿ‘ท @Valid๋กœ ์‰ฝ๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š” ์Šคํ”„๋ง ๋ถ€ํŠธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
Decorative logo