• ABOUT
  • PORTFOLIO
  • POSTS
  • GUESTBOOK

ยฉ 2025 BlueCool12 All rights reserved.

2025.07.01Next.js

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

๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”(SEO)๋Š” ์›น ์„œ๋น„์Šค์˜ ํŠธ๋ž˜ํ”ฝ๊ณผ ์ง๊ฒฐ๋˜๋Š” ์ค‘์š”ํ•œ ์š”์†Œ๋‹ค. React ๊ธฐ๋ฐ˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ SEO๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์ ์šฉํ•˜๋ ค๋ฉด ๋‹จ์ˆœํ•œ CSR ๊ตฌ์กฐ๋ฅผ ๋„˜์–ด์„œ๋Š” ์ „๋žต์ด ํ•„์š”ํ•˜๋‹ค.

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


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

Next.js์˜ ๋ Œ๋”๋ง ์ „๋žต์€ SEO ์„ฑ๋Šฅ๊ณผ ์ง๊ฒฐ๋˜๋Š” ํ•ต์‹ฌ ์š”์†Œ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„ ํฌ๋กค๋Ÿฌ(Googlebot, Yeti)๋Š” ํŽ˜์ด์ง€์— ์ ‘๊ทผํ•˜๋ฉด ๋จผ์ € HTML ๋ฌธ์„œ๋ฅผ ์ฝ๊ณ  ์ฝ˜ํ…์ธ ๋ฅผ ๋ถ„์„ํ•œ๋‹ค.

CSR ๋ฐฉ์‹์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์‹คํ–‰๋œ ์ดํ›„์— ํ™”๋ฉด์ด ์™„์„ฑ๋˜๊ธฐ ๋•Œ๋ฌธ์— ์ดˆ๊ธฐ HTML์ด ๋น„์–ด ์žˆ์„ ์ˆ˜ ์žˆ๋‹ค. ์ด๋กœ ์ธํ•ด ํฌ๋กค๋Ÿฌ๊ฐ€ ์ฝ˜ํ…์ธ ๋ฅผ ์ œ๋Œ€๋กœ ์ธ์‹ํ•˜์ง€ ๋ชปํ•˜๊ฑฐ๋‚˜ ์ธ๋ฑ์‹ฑ์ด ์ง€์—ฐ๋  ์œ„ํ—˜์ด ์žˆ๋‹ค.

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


- SSG (Static Site Generation)

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 (Server-Side Rendering)

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 (Incremental Static Regeneration)

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๋Š” ๊ฐ€์žฅ ์งง์€ ์žฌ๊ฒ€์ฆ ์ฃผ๊ธฐ๋ฅผ ํ•ด๋‹น ๋ผ์šฐํŠธ ์ „์ฒด์˜ ๊ธฐ์ค€์œผ๋กœ ๋ณ‘ํ•ฉํ•˜์—ฌ ์ ์šฉํ•œ๋‹ค.


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

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 ์ด๋ฏธ์ง€์ฒ˜๋Ÿผ ๋ณ€๊ฒฝ ๋นˆ๋„๊ฐ€ ๋‚ฎ์€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌํ•  ๋•Œ ์œ ์šฉํ•˜๋‹ค.


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

์‚ฌ์ดํŠธ ๋‚ด์˜ ๋ชจ๋“  ์œ ํšจํ•œ 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 ๊ธฐ๋Šฅ์„ ์‚ฌ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ๊ฐœ์˜ ์‚ฌ์ดํŠธ๋งต์œผ๋กœ ๋ถ„ํ• ํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.


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

๊ฒ€์ƒ‰ ์—”์ง„์€ ํŽ˜์ด์ง€ ๊ฒฝํ—˜์„ ํ‰๊ฐ€ํ•  ๋•Œ ์ฝ”์–ด ์›น ๋ฐ”์ดํƒˆ ์ง€ํ‘œ๋ฅผ ๊ฒ€์ƒ‰ ๋žญํ‚น์— ๋ฐ˜์˜ํ•œ๋‹ค. 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 ์†์„ฑ์€ ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์‚ฌ์šฉ์ž๋ฅผ ์œ„ํ•œ ์›น ์ ‘๊ทผ์„ฑ ์š”์†Œ์ผ ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์ด๋ฏธ์ง€์˜ ์˜๋ฏธ์™€ ๋ฌธ๋งฅ์„ ์ดํ•ดํ•˜๋Š” ๋ฐ ํ™œ์šฉ๋˜๋ฏ€๋กœ ๊ตฌ์ฒด์ ์œผ๋กœ ์ž‘์„ฑํ•˜๋Š” ๊ฒƒ์ด ์ข‹๋‹ค.


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



๐Ÿ“ ์›น์‚ฌ์ดํŠธ ์„ฑ๋Šฅ ์ธก์ • ๋„๊ตฌ Lighthouse

์ด์ „ ๊ธ€
๐Ÿ‘พ ๋ฆฌ๋ˆ…์Šค ๊ธฐ๋ณธ ๋ช…๋ น์–ด ์ •๋ฆฌ
๋‹ค์Œ ๊ธ€
๐Ÿ–ผ๏ธ ์›น ์„ฑ๋Šฅ์„ ์œ„ํ•œ ์ด๋ฏธ์ง€ ์••์ถ• ๋„๊ตฌ ์ •๋ฆฌ (TinyPNG & Squoosh)
์žฅ์‹์šฉ ๋กœ๊ณ