SEO(Search Engine Optimization)๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ํธ๋ํฝ์ ์ข์ฐํ๋ ํต์ฌ ์์๋ค. React์ ๊ธฐ๋ณธ ๋ ๋๋ง ๋ฐฉ์์ธ CSR(Client-Side Rendering)๋ง์ผ๋ก๋ ๊ฒ์ ์์ง ํฌ๋กค๋ฌ์๊ฒ ์ ๋๋ก ๋
ธ์ถํ๊ธฐ ์ด๋ ต๋ค.
Next.js๋ฅผ ํ์ฉํ๋ฉด ํฌ๋กค๋ฌ๊ฐ ์ฝํ
์ธ ๋ฅผ ๋น ์ง์์ด ์์งํ๊ณ , ๊ฒ์ ๊ฒฐ๊ณผ์๋ ์ ํํ ๋
ธ์ถ๋๋ ํ์ด์ง๋ฅผ ๋ง๋ค ์ ์๋ค.
๊ฒ์ ์์ง ํฌ๋กค๋ฌ(Googlebot, Yeti ๋ฑ)๋ ํ์ด์ง์ ์ ๊ทผํ๋ฉด ๋จผ์ HTML ๋ฌธ์๋ฅผ ์ฝ๊ณ ์ฝํ
์ธ ๋ฅผ ๋ถ์ํ๋ค. CSR ๋ฐฉ์์ ํด๋ผ์ด์ธํธ์์ ์๋ฐ์คํฌ๋ฆฝํธ๊ฐ ์คํ๋ ํ์์ผ ํ๋ฉด์ด ์์ฑ๋๋ฏ๋ก ์ด๊ธฐ HTML์ด ๋น์ด ์์ ์ ์๋ค. ์ด ๊ฒฝ์ฐ ํฌ๋กค๋ฌ๊ฐ ์ฝํ
์ธ ๋ฅผ ์ ๋๋ก ์ธ์ํ์ง ๋ชปํ๊ฑฐ๋ ์ธ๋ฑ์ฑ์ด ์ง์ฐ๋ ์ ์๋ค.
๋ฐ๋ฉด ์๋ฒ์์ ๋ฏธ๋ฆฌ ์์ฑ๋ HTML์ ์ ๊ณตํ๋ฉด ํฌ๋กค๋ฌ๊ฐ ์ฆ์ ์ฝํ
์ธ ๋ฅผ ์์งํ ์ ์์ด SEO์ ์ ๋ฆฌํ๋ค. Next.js๋ ์ํฉ์ ๋ง๊ฒ ์ ํํ ์ ์๋ ์ฌ๋ฌ ๋ ๋๋ง ์ ๋ต์ ์ง์ํ๋ค.
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๋ ์น ์ ํ๋ฆฌ์ผ์ด์
์ ์๋ฒ์ ๋ฐฐํฌํ๊ธฐ ์ ๋น๋ ์์ ์ ๊ฐ ํ์ด์ง์ 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์ ์ ์ฒด ์ฌ์ดํธ๋ฅผ ๋ค์ ๋น๋ ํ์ง ์๊ณ ๋ ํน์ ์ฃผ๊ธฐ๋ ์กฐ๊ฑด์ ๋ฐ๋ผ ๊ธฐ์กด์ ์ ์ ํ์ด์ง(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 ํจ์์ ๊ฐ๋ณ ์บ์ ์ค์ ์ด ํจ๊ป ์์ผ๋ฉด ๋ ์งง์ ์ฌ๊ฒ์ฆ ์ฃผ๊ธฐ๋ฅผ ๋ผ์ฐํธ ์ ์ฒด์ ๊ธฐ์ค์ผ๋ก ๋ณํฉํด ์ ์ฉํ๋ค.
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 ์ด๋ฏธ์ง์ฒ๋ผ ๋ณ๊ฒฝ ๋น๋๊ฐ ๋ฎ์ ์ ์ ๋ฆฌ์์ค๋ฅผ ๊ด๋ฆฌํ ๋ ์ ์ฉํ๋ค.
๊ฒ์ ์์ง ํฌ๋กค๋ฌ๋ 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',
};
}
๋์ ๋ฐฉ์์ ์ฐ๋ฉด ํ๊ฒฝ ๋ณ์์ ๋ฐ๋ผ ํฌ๋กค๋ง ํ์ฉ ์ฌ๋ถ๋ฅผ ๋ถ๊ธฐ ์ฒ๋ฆฌํ ์ ์๋ค.
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];
}โ
๊ฒ์ ์์ง์ ํ์ด์ง ๊ฒฝํ์ ํ๊ฐํ ๋ ์ฝ์ด ์น ๋ฐ์ดํ ์งํ๋ฅผ ๊ฒ์ ๋ญํน์ ๋ฐ์ํ๋ค. 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 ์์ฑ์ ์คํฌ๋ฆฐ ๋ฆฌ๋ ์ฌ์ฉ์๋ฅผ ์ํ ์ ๊ทผ์ฑ ์์์ด์, ๊ฒ์ ์์ง์ด ์ด๋ฏธ์ง์ ์๋ฏธ์ ๋งฅ๋ฝ์ ์ดํดํ๋ ๋ฐ๋ ์ฐ์ด๋ฏ๋ก ๊ตฌ์ฒด์ ์ผ๋ก ์์ฑํ๋ ๊ฒ ์ข๋ค.
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์ ํฌํจ๋ ์ ๋ณด๋ ์ค์ ํ์ด์ง ํ๋ฉด์ ๋ ธ์ถ๋๋ ์ฝํ ์ธ ์ ์ผ์นํด์ผ ํ๋ค. ํ๋ฉด์ ๋ณด์ด์ง ์๋ ์จ๊ฒจ์ง ๋ฐ์ดํฐ๋ง ๊ตฌ์กฐํ๋ ๋ฐ์ดํฐ๋ก ์ ๊ณตํ๋ฉด ๊ฒ์ ์์ง ์คํธ ์ ์ฑ ์๋ฐ์ผ๋ก ๊ฐ์ฃผ๋ ์ ์๋ค.
