๊ฒ์ ์์ง ์ต์ ํ(SEO)๋ ์น ์๋น์ค์ ํธ๋ํฝ๊ณผ ์ง๊ฒฐ๋๋ ์ค์ํ ์์๋ค. React ๊ธฐ๋ฐ ์ ํ๋ฆฌ์ผ์ด์
์์ SEO๋ฅผ ํจ๊ณผ์ ์ผ๋ก ์ ์ฉํ๋ ค๋ฉด ๋จ์ํ CSR ๊ตฌ์กฐ๋ฅผ ๋์ด์๋ ์ ๋ต์ด ํ์ํ๋ค.
Next.js๋ ์๋ฒ ์ฌ์ด๋ ๋ ๋๋ง๊ณผ ์ ์ ์์ฑ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ฌ ๊ฒ์ ์์ง ์นํ์ ์ธ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ๊ตฌ์ถํ ์ ์๋๋ก ๋๋๋ค. ์ด๋ฒ ๊ธ์์๋ Next.js์์ SEO๋ฅผ ์ต์ ํํ๋ ํต์ฌ ๋ฐฉ๋ฒ๋ค์ ์ ๋ฆฌํ์๋ค.
Next.js์ ๋ ๋๋ง ์ ๋ต์ SEO ์ฑ๋ฅ๊ณผ ์ง๊ฒฐ๋๋ ํต์ฌ ์์๋ค. ๊ฒ์ ์์ง ํฌ๋กค๋ฌ(Googlebot, Yeti)๋ ํ์ด์ง์ ์ ๊ทผํ๋ฉด ๋จผ์ HTML ๋ฌธ์๋ฅผ ์ฝ๊ณ ์ฝํ
์ธ ๋ฅผ ๋ถ์ํ๋ค.
CSR ๋ฐฉ์์ ํด๋ผ์ด์ธํธ์์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋ ์ดํ์ ํ๋ฉด์ด ์์ฑ๋๊ธฐ ๋๋ฌธ์ ์ด๊ธฐ HTML์ด ๋น์ด ์์ ์ ์๋ค. ์ด๋ก ์ธํด ํฌ๋กค๋ฌ๊ฐ ์ฝํ
์ธ ๋ฅผ ์ ๋๋ก ์ธ์ํ์ง ๋ชปํ๊ฑฐ๋ ์ธ๋ฑ์ฑ์ด ์ง์ฐ๋ ์ํ์ด ์๋ค.
๋ฐ๋ฉด ์๋ฒ์์ ๋ฏธ๋ฆฌ ์์ฑ๋ HTML์ ์ ๊ณตํ๋ SSR ๋ฐฉ์์ ํฌ๋กค๋ฌ๊ฐ ์ฆ์ ์ฝํ
์ธ ๋ฅผ ์์งํ ์ ์์ด SEO์ ์ ๋ฆฌํ๋ค. ์ด๋ฌํ ์ฅ์ ์ ๊ธฐ๋ฐ์ผ๋ก Next.js๋ SSR์ ํฌํจํด SSG, ISR๊น์ง ์ธ ๊ฐ์ง ๋ ๋๋ง ์ ๋ต์ ์ง์ํ์ฌ ์ํฉ์ ๋ง๋ SEO ์ต์ ํ๊ฐ ๊ฐ๋ฅํ๋ค.
SSG๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ์๋ฒ์ ๋ฐฐํฌํ๊ธฐ ์ ๋น๋ ์์ ์ ๊ฐ ํ์ด์ง์ HTML์ ๋ฏธ๋ฆฌ ์์ฑํด ๋๋ ๋ ๋๋ง ๋ฐฉ์์ด๋ค. ์ฌ์ฉ์๊ฐ ํ์ด์ง๋ฅผ ์์ฒญํ๋ฉด ์๋ฒ๋ ์ด๋ฏธ ๋ง๋ค์ด์ง ์ ์ HTML ํ์ผ์ ์ฆ์ ๋ฐํํ๋ค.
Next.js App Router ํ๊ฒฝ์์ SSG๋ฅผ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ฐ์ดํฐ ํจ์นญ ์ ๋ฌด์ ๋ผ์ฐํธ ํํ์ ๋ฐ๋ผ ๋ค์๊ณผ ๊ฐ์ด ์ ์ฉํ๋ค.
1. ๋ฐ์ดํฐ ํจ์นญ์ด ์๋ ํ์ด์ง fetch ํจ์๋ ๋์ ํจ์๋ฅผ ์ฌ์ฉํ์ง ์๋ ์์ UI ์ปดํฌ๋ํธ๋ ์์คํ
์ด ๋น๋ ์์ ์ ์๋์ผ๋ก ์ ์ HTML๋ก ์์ฑํ๋ค.
2. ๋ฐ์ดํฐ ํจ์นญ์ด ํฌํจ๋ ํ์ด์ง
API ๋ฑ ์ธ๋ถ ๋ฐ์ดํฐ๋ฅผ ํธ์ถํ์ฌ ํ์ด์ง๋ฅผ ๊ตฌ์ฑํ๋ ๊ฒฝ์ฐ fetch ์์ฒญ ์ ์บ์ฑ ์ต์
์ ๋ช
์ํด์ผ ํ๋ค. Next.js 15 ๋ฒ์ ๋ถํฐ fetch์ ๊ธฐ๋ณธ๊ฐ์ด ์บ์ฑ ์์(no-store)์ผ๋ก ๋ณ๊ฒฝ๋์๊ธฐ ๋๋ฌธ์ ๋น๋ ์์ ์ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ ์ ์ ํ์ด์ง๋ก ๊ณ ์ ํ๋ ค๋ฉด 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>;
}
3. ๋์ ๋ผ์ฐํธ ํ์ด์ง ([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),
}));
}
4. ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ์ค์ ๊ฐ์
ํน์ ๋ผ์ฐํธ ํ์ผ(page.tsx ๋๋ layout.tsx) ์ต์๋จ์ ๋ ๋๋ง ๋ฐฉ์์ ๋ช
์์ ์ผ๋ก ์ ์ธํ์ฌ ํ์ผ ๋ด๋ถ์ ๋ก์ง๊ณผ ๋ฌด๊ดํ๊ฒ ํด๋น ํ์ด์ง ์ ์ฒด๋ฅผ SSG๋ก ๊ฐ์ ํ ์ ์๋ค.
export const dynamic = 'force-static';
SSR์ ์ฌ์ฉ์์ ํ์ด์ง ์์ฒญ์ด ๋ฐ์ํ ๋๋ง๋ค ์๋ฒ์์ ๋ฐ์ดํฐ๋ฅผ ์๋ก ๊ฐ์ ธ์ HTML์ ์ค์๊ฐ์ผ๋ก ๋ ๋๋งํ์ฌ ๋ฐํํ๋ ๋ฐฉ์์ด๋ค.
Next.js App Router ํ๊ฒฝ์์ SSR์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. ์บ์ฑ ๋นํ์ฑํ๋ฅผ ํตํ ๋์ ๋ ๋๋ง ์ ๋ 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>
}
2. ๋์ ํจ์ ์ฌ์ฉ 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>;
}โ
3. ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ์ค์ ๊ฐ์
ํน์ ๋ผ์ฐํธ ํ์ผ ์ต์๋จ์ ๋ ๋๋ง ๋ฐฉ์์ ๋ช
์์ ์ผ๋ก ์ ์ธํ์ฌ ๋ด๋ถ ๊ตฌํ๊ณผ ๋ฌด๊ดํ๊ฒ ํด๋น ํ์ด์ง ์ ์ฒด๋ฅผ SSR๋ก ๊ฐ์ ํ ์ ์๋ค.
export const dynamic = 'force-dynamic';
ISR์ ์ ์ฒด ์ฌ์ดํธ๋ฅผ ๋ค์ ๋น๋ํ์ง ์๊ณ ๋ ํน์ ์ฃผ๊ธฐ๋ ์กฐ๊ฑด์ ๋ฐ๋ผ ๊ธฐ์กด์ ์ ์ ํ์ด์ง(SSG)๋ฅผ ๋ฐฑ๊ทธ๋ผ์ด๋์์ ์
๋ฐ์ดํธํ๋ ํ์ด๋ธ๋ฆฌ๋ ๋ ๋๋ง ๋ฐฉ์์ด๋ค. ์ฌ์ฉ์์๊ฒ๋ ํญ์ ์บ์๋ ๋น ๋ฅธ ์ ์ ํ์ด์ง๋ฅผ ์ ๊ณตํ๋ฉด์๋ ๋ฐ์ดํฐ์ ์ต์ ์ฑ์ ์ ์งํ ์ ์๋ค.
Next.js App Router ํ๊ฒฝ์์ ISR์ ๊ตฌํํ๋ ๋ฐฉ๋ฒ์ ๋ค์๊ณผ ๊ฐ๋ค.
1. ์๊ฐ ๊ธฐ๋ฐ ์ฌ๊ฒ์ฆ 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>;
}
2. ์จ๋๋งจ๋ ์ฌ๊ฒ์ฆ
์๊ฐ ์ฃผ๊ธฐ์ ์์กดํ์ง ์๊ณ ๊ฐ์ด ๋ณ๊ฒฝ๋ ์ฆ์ ํน์ ํ์ด์ง๋ ๋ฐ์ดํฐ์ ์บ์๋ฅผ ์๋์ผ๋ก ๊ฐ์ ๋ฌดํจํํ๋ค. fetch์ ํ๊ทธ๋ฅผ ์ง์ ํ๊ณ ๋ฐ์ดํฐ ์
๋ฐ์ดํธ ์์ ์ ๋ผ์ฐํธ ํธ๋ค๋ฌ ๋ฑ์์ revalidateTag ๋๋ revalidatePath๋ฅผ ํธ์ถํ๋ค.
// 1. ๊ณ ์ ํ๊ทธ ๋ถ์ฌ
const res = await fetch('https://api.pyomin.com/posts', {
next: { tags: ['posts'] },
});
// ----------------------------------------------------
// 2. ์ธ๋ถ API ๋๋ ๋ผ์ฐํธ ํธ๋ค๋ฌ์์ ๋ฐ์ดํฐ ์์ ์ ํ๊ทธ ๊ธฐ๋ฐ ์บ์ ๋ฌดํจํ ์คํ
import { revalidateTag } from 'next/cache';
export async function POST(request: Request) {
// DB ์ ๋ฐ์ดํธ ๋ก์ง ์ดํ...
revalidateTag('posts')โ; // 'posts' ํ๊ทธ๊ฐ ๋ถ์ ๋ชจ๋ fetch ์บ์ ์ฆ์ ์ญ์ ๋ฐ ์ฌ์์ฑ ์ ๋
return Response.json({ revalidated: true });
}
3. ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ์ค์ ๊ฐ์
ํน์ ๋ผ์ฐํธ ํ์ผ ์ต์๋จ์ revalidate ๋ณ์๋ฅผ ์ ์ธํ์ฌ ํ์ผ ๋ด๋ถ์ ๋ชจ๋ ๋ฐ์ดํฐ ํจ์นญ์ด๋ ๋ ๋๋ง์ ์ฌ๊ฒ์ฆ ์ฃผ๊ธฐ๋ฅผ ์ผ๊ด์ ์ผ๋ก ์ค์ ํ ์ ์๋ค.
// ํด๋น ๋ผ์ฐํธ์ ๋ชจ๋ ๋ ๋๋ง ๊ฒฐ๊ณผ๋ฅผ 3600์ด(1์๊ฐ)๋ง๋ค ์ฌ๊ฒ์ฆ
export const revalidate = 3600;
์ฐธ๊ณ ) ํ์ด์ง ๋จ์์ ๋ผ์ฐํธ ์ธ๊ทธ๋จผํธ ์ค์ ๊ณผ ์ปดํฌ๋ํธ ๋ด๋ถ fetchํจ์์ ๊ฐ๋ณ ์บ์ ์ค์ ์ด ํผ์ฌํ ๊ฒฝ์ฐ Next.js๋ ๊ฐ์ฅ ์งง์ ์ฌ๊ฒ์ฆ ์ฃผ๊ธฐ๋ฅผ ํด๋น ๋ผ์ฐํธ ์ ์ฒด์ ๊ธฐ์ค์ผ๋ก ๋ณํฉํ์ฌ ์ ์ฉํ๋ค.
Next.js์์๋ ๊ธฐ์กด์ next/head ์ปดํฌ๋ํธ๋ฅผ ๋์ฒดํ๋ Metadata API๋ฅผ ์ ๊ณตํ๋๋ฐ <head> ์์ญ์ ํฌํจ๋๋ title, description, Open Graph ์ ๋ณด ๋ฑ์ ์๋ฒ ์ธก์์ ๋ ๋๋งํ์ฌ ๊ฒ์ ์์ง ํฌ๋กค๋ฌ๊ฐ ๋ช
ํํ ํ์ด์ง ์ ๋ณด๋ฅผ ์์งํ ์ ์๋๋ก ๋๋๋ค.
ํ์ด์ง์ ๋ฉํ๋ฐ์ดํฐ๊ฐ ๊ณ ์ ๋์ด ์๋ ๊ฒฝ์ฐ ์ฌ์ฉํ๋ค. 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 ๋น๋๊ธฐ ํจ์๋ฅผ ํตํด ๋ฉํ๋ฐ์ดํฐ ๊ฐ์ฒด๋ฅผ ๋์ ์ผ๋ก ์์ฑํ ์ ์๋ค.
๋ํ generateMetadata ๋ด๋ถ์ fetch ์์ฒญ๊ณผ ํ์ด์ง ์ปดํฌ๋ํธ์์ ๋์ผํ API๋ฅผ ํธ์ถํ๋ ๊ฒฝ์ฐ Next.js๋ ์๋์ผ๋ก ์์ฒญ์ ๋ฉ๋ชจ์ด์ ์ด์
ํ๋ค. ๋ฐ๋ผ์ ๋์ผํ ๋ฐ์ดํฐ ์์ฒญ์ด ๋ฐ์ํ๋๋ผ๋ ์ค์ ๋คํธ์ํฌ ์์ฒญ์ ํ ๋ฒ๋ง ์ํ๋๋ค.
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}`,
},
};
}โ
ํ์ด์ง ์ ๋ชฉ์ ๊ณตํต ์ ๋ฏธ์ฌ ๋๋ ์ ๋์ฌ๋ฅผ ์๋์ผ๋ก ์ถ๊ฐํ ๋๋ layout.tsx์ ๊ฐ์ ์์ ๋ ์ด์์ ํ์ผ์์ ๋ฉํ๋ฐ์ดํฐ ํ
ํ๋ฆฟ์ ์ค์ ํ ์ ์๋ค. title.template์ ํ์ฉํ๋ฉด ํ์ ํ์ด์ง์์ ์ ๋ชฉ์ ์์ฑํ ๋ ๋ฐ๋ณต๋๋ ๋ฌธ์์ด์ ์๋์ผ๋ก ๋ถ์ผ ์ ์๋ค.
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 ์ด๋ฏธ์ง์ฒ๋ผ ๋ณ๊ฒฝ ๋น๋๊ฐ ๋ฎ์ ๋ฉํ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํ ๋ ์ ์ฉํ๋ค.
๊ฒ์ ์์ง์ ์น์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํ ๋ ๊ฐ์ฅ ๋จผ์ robots.txt๋ฅผ ์ฝ์ด ํฌ๋กค๋ง ํ์ฉ ๋ฒ์๋ฅผ ํ์
ํ๊ณ sitemap.xml์ ํตํด ์ ์ฒด ํ์ด์ง ๊ตฌ์กฐ์ ์ต์ ์
๋ฐ์ดํธ ๋ด์ญ์ ์์งํ๋ค. Next.js์์๋ ์ ์ ํ์ผ์ ์ง์ ๋ฐฐ์นํ๊ฑฐ๋ ์ฝ๋๋ฅผ ํตํ ๋์ ์์ฑ ๋ฐฉ์์ ๋ชจ๋ ์ง์ํ๋ค.
๊ฒ์ ์์ง ํฌ๋กค๋ฌ๊ฐ ์ ๊ทผํ ์ ์๋ ๊ฒฝ๋ก์ ์ ๊ทผ์ ์ฐจ๋จํ ๊ฒฝ๋ก๋ฅผ ์ ์ํ๋ค. 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',
};
}
๋์ ๋ฐฉ์์ ์ฌ์ฉํ๋ฉด ํ๊ฒฝ ๋ณ์์ ๋ฐ๋ผ ํฌ๋กค๋ง ํ์ฉ ์ฌ๋ถ๋ฅผ ๋ถ๊ธฐ ์ฒ๋ฆฌํ๊ธฐ ์ ๋ฆฌํ๋ค๋ ์ฅ์ ์ด ์๋ค.
์ฌ์ดํธ ๋ด์ ๋ชจ๋ ์ ํจํ URL ๋ชฉ๋ก๊ณผ ๊ฐ ํ์ด์ง์ ๋ง์ง๋ง ์์ ์ผ, ๋ณ๊ฒฝ ๋น๋, ์ค์๋๋ฅผ ๊ฒ์ ์์ง์ ์ ๊ณตํ์ฌ ์ธ๋ฑ์ฑ ํจ์จ์ ๋์ธ๋ค. app/sitemap.ts ํ์ผ์ ์์ฑํ์ฌ MetadataRoute.Sitemap ๋ฐฐ์ด์ ๋ฐํํ๋ ๋น๋๊ธฐ ํจ์๋ฅผ ์์ฑํ๋ค.
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];
}โ
๊ตฌ๊ธ์ ์ฌ์ดํธ๋งต ๊ท๊ฒฉ์ ๋ฐ๋ผ ๋จ์ผ ์ฌ์ดํธ๋งต ํ์ผ์ ์ต๋ 5๋ง๊ฐ์ URL๊น์ง๋ง ํฌํจํ ์ ์์ผ๋ฏ๋ก ๋์ ๊ฒฝ๋ก๊ฐ ์ด๋ฅผ ์ด๊ณผํ ๊ฒฝ์ฐ generateSitemaps ๊ธฐ๋ฅ์ ์ฌ์ฉํ์ฌ ์ฌ๋ฌ ๊ฐ์ ์ฌ์ดํธ๋งต์ผ๋ก ๋ถํ ํ๋ ๊ฒ์ด ์ข๋ค.
๊ฒ์ ์์ง์ ํ์ด์ง ๊ฒฝํ์ ํ๊ฐํ ๋ ์ฝ์ด ์น ๋ฐ์ดํ ์งํ๋ฅผ ๊ฒ์ ๋ญํน์ ๋ฐ์ํ๋ค. Next.js์ Image ์ปดํฌ๋ํธ๋ ์ด ์ค์์๋ LCP(์ต๋ ์ฝํ ์ธ ํ ํ์ธํธ)์ CLS(๋์ ๋ ์ด์์ ์ด๋)๋ฅผ ์์คํ ๋ ๋ฒจ์์ ์ต์ ํํ๋๋ก ์ค๊ณ๋์ด ์๋ค.
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>
);
}
Image ์ปดํฌ๋ํธ๋ width์ height ์์ฑ์ ํ์๋ก ์๊ตฌํ์ฌ ๋ ๋๋ง ์์ ์ ์ด๋ฏธ์ง ์์ญ์ ๋ฏธ๋ฆฌ ํ๋ณดํ๋ค. ์ด๋ฅผ ํตํด ๋ ์ด์์ ์ด๋์ ์์ฒ์ ์ผ๋ก ๋ฐฉ์งํ ์ ์๋ค. ๋ํ ๊ธฐ๋ณธ์ ์ผ๋ก ์ง์ฐ ๋ก๋ฉ์ด ์ ์ฉ๋๋ฉฐ WebP ๋ฐ AVIF์ ๊ฐ์ ํ๋์ ์ด๋ฏธ์ง ํฌ๋งท์ผ๋ก ์๋ ๋ณํ๋์ด ์ฑ๋ฅ์ ์ต์ ํํ๋ค.
ํ์ด์ง ์ต์๋จ์ ์์นํ์ฌ ํ๋ฉด์์ ๊ฐ์ฅ ํฐ ๋น์ค์ ์ฐจ์งํ๋ ํต์ฌ ์ด๋ฏธ์ง์ ๊ฒฝ์ฐ ์ง์ฐ ๋ก๋ฉ์ด ์ ์ฉ๋๋ฉด ์คํ๋ ค ์ฑ๋ฅ ์งํ๊ฐ ํ๋ฝํ ์ ์๊ธฐ ๋๋ฌธ์ LCP ๊ฐ์ ์ ์ํด priority ์์ฑ์ ๋ถ์ฌํ์ฌ ๋ธ๋ผ์ฐ์ ๊ฐ ํด๋น ์ด๋ฏธ์ง๋ฅผ ์ต์ฐ์ ์ผ๋ก ๋ก๋ํ๋๋ก ์ค์ ํ๋ ๊ฒ์ด ์ข๋ค.
๋ํ alt ์์ฑ์ ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ฌ์ฉ์๋ฅผ ์ํ ์น ์ ๊ทผ์ฑ ์์์ผ ๋ฟ๋ง ์๋๋ผ ๊ฒ์ ์์ง์ด ์ด๋ฏธ์ง์ ์๋ฏธ์ ๋ฌธ๋งฅ์ ์ดํดํ๋ ๋ฐ ํ์ฉ๋๋ฏ๋ก ๊ตฌ์ฒด์ ์ผ๋ก ์์ฑํ๋ ๊ฒ์ด ์ข๋ค.
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์ ํฌํจ๋ ์ ๋ณด๋ ์ค์ ํ์ด์ง ํ๋ฉด์ ๋ ธ์ถ๋๋ ์ฝํ ์ธ ์ ์ผ์นํด์ผ ํ๋ค. ํ๋ฉด์ ๋ณด์ด์ง ์๋ ์จ๊ฒจ์ง ๋ฐ์ดํฐ๋ง ๊ตฌ์กฐํ ๋ฐ์ดํฐ๋ก ์ ๊ณตํ๋ ๊ฒ์ ๊ฒ์ ์์ง ์คํธ ์ ์ฑ ์๋ฐ์ผ๋ก ๊ฐ์ฃผ๋ ์ ์์ผ๋ฏ๋ก ์ฃผ์ํด์ผ ํ๋ค.
