[๐Ÿค–] Next.js App Router์˜ `generateMetadata`์™€ `generateViewport` ์‹ฌํ™”: SEO ๋ฐ PWA ์ตœ์ ํ™” ์ „๋žต

Next.js 14/13 App Router์—์„œ `generateMetadata`์™€ `generateViewport` ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•˜์—ฌ ๋™์  SEO ๋ฉ”ํƒ€ํƒœ๊ทธ์™€ PWA ์„ค์ •์„ ์ตœ์ ํ™”ํ•˜๋Š” ์‹ฌํ™” ์ „๋žต์„ ๋‹ค๋ฃน๋‹ˆ๋‹ค. ์‹ค๋ฌด ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ ์›น์‚ฌ์ดํŠธ์˜ ๊ฒ€์ƒ‰ ์—”์ง„ ๋…ธ์ถœ ๋ฐ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์„ธ์š”.

23๋ถ„
๋‹จ์–ด: 2,136๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js 14/13 App Router ํ™˜๊ฒฝ์—์„œ generateMetadata์™€ generateViewport ํ•จ์ˆ˜๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์›น์‚ฌ์ดํŠธ์˜ SEO(๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”)์™€ PWA(ํ”„๋กœ๊ทธ๋ ˆ์‹œ๋ธŒ ์›น ์•ฑ) ๊ฒฝํ—˜์„ ๊ทน๋Œ€ํ™”ํ•˜๋Š” ์‹ฌํ™” ์ „๋žต์„ ์‹ค์šฉ์ ์ธ ์ฝ”๋“œ ์˜ˆ์‹œ์™€ ํ•จ๊ป˜ ์ž์„ธํžˆ ์•Œ์•„๋ด์š”.

์•ˆ๋…•ํ•˜์„ธ์š”, ๋ธ”๋ฃจ์˜ˆ์š”! Next.js App Router๋Š” ๊ฐœ๋ฐœ ์ƒ์‚ฐ์„ฑ๊ณผ ์„ฑ๋Šฅ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œ์ผฐ์ง€๋งŒ, ์ „ํ†ต์ ์ธ SEO ๋ฐ PWA ์„ค์ • ๋ฐฉ์‹๊ณผ๋Š” ๋‹ค๋ฅธ ์ ‘๊ทผ์ด ํ•„์š”ํ•ด์š”.
ํŠนํžˆ generateMetadata์™€ generateViewport ํ•จ์ˆ˜๋Š” App Router ํ™˜๊ฒฝ์—์„œ ์›น์‚ฌ์ดํŠธ์˜ ๊ฒ€์ƒ‰ ์—”์ง„ ๋…ธ์ถœ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ฒฐ์ •ํ•˜๋Š” ํ•ต์‹ฌ ์š”์†Œ์ธ๋ฐ์š”.
์˜ค๋Š˜์€ ์ด ๋‘ ํ•จ์ˆ˜๋ฅผ ์‹ค๋ฌด์—์„œ ์–ด๋–ป๊ฒŒ ํšจ๊ณผ์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ SEO ์ ์ˆ˜๋ฅผ ๋†’์ด๊ณ , PWA ๊ธฐ๋Šฅ์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ์‹ฌ๋„ ์žˆ๊ฒŒ ๋‹ค๋ค„๋ณด๋ ค๊ณ  ํ•ด์š”.

0๏ธโƒฃ ์™œ ์ด ๊ธฐ๋Šฅ๋“ค์ด ํ•„์ˆ˜์ ์ธ๊ฐ€์š”?

์›น์‚ฌ์ดํŠธ์˜ ์„ฑ๊ณต์€ ๋‹จ์ˆœํžˆ ๊ธฐ๋Šฅ ๊ตฌํ˜„์„ ๋„˜์–ด, ๊ฒ€์ƒ‰ ์—”์ง„์—์„œ ์ž˜ ๋…ธ์ถœ๋˜๊ณ  ์‚ฌ์šฉ์ž๋“ค์ด ํŽธ๋ฆฌํ•˜๊ฒŒ ์ด์šฉํ•  ์ˆ˜ ์žˆ๋Š”์ง€์— ๋‹ฌ๋ ค์žˆ์–ด์š”.
generateMetadata๋Š” ์›นํŽ˜์ด์ง€์˜ ์ œ๋ชฉ, ์„ค๋ช…, ํ‚ค์›Œ๋“œ, Open Graph ์ด๋ฏธ์ง€ ๋“ฑ SEO์˜ ํ•ต์‹ฌ ์š”์†Œ๋“ค์„ ๋™์ ์œผ๋กœ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๊ณ ์š”.
์ด๋ฅผ ํ†ตํ•ด ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—์„œ ๋” ๋งค๋ ฅ์ ์œผ๋กœ ๋ณด์ด๊ณ , ์†Œ์…œ ๋ฏธ๋””์–ด ๊ณต์œ  ์‹œ ํ’๋ถ€ํ•œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.
generateViewport๋Š” ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ์—์„œ์˜ ๋ฐ˜์‘ํ˜• ๋™์ž‘, PWA ์„ค์น˜ ์‹œ ์•ฑ ์•„์ด์ฝ˜ ์ƒ‰์ƒ ๋“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ์ฃผ๋Š” ๋ทฐํฌํŠธ ๊ด€๋ จ ์„ค์ •์„ ๋‹ด๋‹นํ•ด์š”.
์ด ๋‘ ํ•จ์ˆ˜๋ฅผ ์ž˜ ํ™œ์šฉํ•˜๋ฉด ์›น์‚ฌ์ดํŠธ์˜ ์ ‘๊ทผ์„ฑ๊ณผ ์‚ฌ์šฉ์ž ์นœํ™”์„ฑ์„ ํฌ๊ฒŒ ํ–ฅ์ƒ์‹œํ‚ฌ ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„์™€ App Router์˜ ๋ณ€ํ™”

Pages Router ์‹œ์ ˆ์—๋Š” _document.js๋‚˜ ๊ฐ ํŽ˜์ด์ง€์˜ Head ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”ํƒ€ํƒœ๊ทธ๋ฅผ ๊ด€๋ฆฌํ–ˆ์–ด์š”.
์ด๋Š” ์ •์ ์ธ ํŽ˜์ด์ง€์—์„œ๋Š” ๋ฌธ์ œ๊ฐ€ ์—†์—ˆ์ง€๋งŒ, ๋™์ ์ธ ๋ฐ์ดํ„ฐ์— ๋”ฐ๋ผ ๋ฉ”ํƒ€ํƒœ๊ทธ๋ฅผ ๋ณ€๊ฒฝํ•ด์•ผ ํ•  ๋•Œ ๋ณต์žกํ•œ ๋กœ์ง์ด ํ•„์š”ํ–ˆ์ฃ .
๋˜ํ•œ, ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ๋ Œ๋”๋ง(SSR) ํ™˜๊ฒฝ์—์„œ ๋ฉ”ํƒ€ํƒœ๊ทธ๊ฐ€ ์ œ๋Œ€๋กœ ์ฃผ์ž…๋˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ๋„ ์‰ฝ์ง€ ์•Š์•˜์–ด์š”.
App Router์—์„œ๋Š” generateMetadata์™€ generateViewport๊ฐ€ Server Component์—์„œ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์—, ๋ฐ์ดํ„ฐ ํŽ˜์นญ๊ณผ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ์„ ํ•œ ๊ณณ์—์„œ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”.
์ด๋Š” ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ์„ ๋†’์ด๊ณ , SEO ๋ด‡์ด ํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•  ๋•Œ ํ•ญ์ƒ ์ตœ์‹  ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค€๋‹ต๋‹ˆ๋‹ค.

generateMetadata๋Š” layout.tsx๋‚˜ page.tsx ํŒŒ์ผ์—์„œ ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋กœ ์ •์˜ํ•˜์—ฌ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์—์„œ ์ •์˜๋œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์ž์‹ ๋ผ์šฐํŠธ์—์„œ ์ƒ์†๋ฐ›๊ฑฐ๋‚˜ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•  ์ˆ˜ ์žˆ์–ด์„œ ์œ ์—ฐํ•œ ๊ด€๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•ด์š”.

0๏ธโƒฃ ๊ธฐ๋ณธ ์‚ฌ์šฉ๋ฒ•๊ณผ ์ •์ /๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์‚ฌ์šฉ๋ฒ•์€ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐฉ์‹์ด์—์š”.
์ •์ ์ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๋‹จ์ˆœํžˆ ๊ฐ์ฒด๋ฅผ ๋‚ด๋ณด๋‚ด๋ฉด ๋˜๊ณ ์š”, ๋™์ ์ธ ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•  ๋•Œ๋Š” params๋‚˜ searchParams๋ฅผ ์ธ์ž๋กœ ๋ฐ›์•„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/layout.tsx ๋˜๋Š” app/page.tsx import { Metadata } from 'next'; export const metadata: Metadata = { title: '๋ธ”๋ฃจ์˜ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ', description: '์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ์˜ ์‹ค์ „ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.', keywords: ['Next.js', 'React', 'TypeScript', 'SEO', '์›น ๊ฐœ๋ฐœ'], authors: [{ name: '๋ธ”๋ฃจ', url: 'https://blue.dev' }], creator: '๋ธ”๋ฃจ', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body>{children}</body> </html> ); }

๋™์ ์ธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๋‹ค์Œ๊ณผ ๊ฐ™์ด generateMetadata ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด์š”.

// app/posts/[slug]/page.tsx import { Metadata } from 'next'; type Props = { params: { slug: string }; searchParams: { [key: string]: string | string[] | undefined }; }; // ๋™์  ๋ฐ์ดํ„ฐ ๊ธฐ๋ฐ˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์„ฑ export async function generateMetadata( { params, searchParams }: Props, ): Promise<Metadata> { const product = await getProductBySlug(params.slug); // ๊ฐ€์ƒ์˜ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ํ•จ์ˆ˜ if (!product) { return { title: 'ํŽ˜์ด์ง€๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด์š”.' }; } return { title: product.name, description: product.description.substring(0, 150) + '...', openGraph: { images: [product.imageUrl], }, }; } export default function PostPage({ params }: Props) { return <h1>{params.slug} ํฌ์ŠคํŠธ ๋‚ด์šฉ์ด์—์š”.</h1>; } async function getProductBySlug(slug: string) { // ์‹ค์ œ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋˜๋Š” API ํ˜ธ์ถœ ๋กœ์ง return { name: `Next.js ${slug} ์‹ฌํ™” ๊ฐ€์ด๋“œ`, description: `์ด๊ฒƒ์€ ${slug}์— ๋Œ€ํ•œ ์‹ฌ๋„ ์žˆ๋Š” ๊ฐ€์ด๋“œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. Next.js ๊ฐœ๋ฐœ์ž๋“ค์ด ์•Œ์•„์•ผ ํ•  ๋ชจ๋“  ๊ฒƒ์„ ๋‹ด์•˜์–ด์š”.`, imageUrl: `https://blue.dev/images/${slug}.jpg`, }; }

์ด๋ ‡๊ฒŒ generateMetadata ํ•จ์ˆ˜๋Š” ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ฐ€์ ธ์™€์„œ ํŽ˜์ด์ง€ ๋‚ด์šฉ์— ๋งž๋Š” ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์œ ์—ฐํ•˜๊ฒŒ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ค˜์š”.

1๏ธโƒฃ Open Graph, Twitter Card ๋“ฑ ์†Œ์…œ ๊ณต์œ  ์ตœ์ ํ™”

์†Œ์…œ ๋ฏธ๋””์–ด ๊ณต์œ  ์‹œ ์›น์‚ฌ์ดํŠธ ๋งํฌ์˜ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ํ’๋ถ€ํ•˜๊ฒŒ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์€ ์‚ฌ์šฉ์ž ์œ ์ž…์— ๋งค์šฐ ์ค‘์š”ํ•ด์š”.
openGraph์™€ twitter ์†์„ฑ์„ ํ™œ์šฉํ•˜์—ฌ ์ œ๋ชฉ, ์„ค๋ช…, ์ด๋ฏธ์ง€, URL ๋“ฑ์„ ์ƒ์„ธํ•˜๊ฒŒ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/posts/[slug]/page.tsx (generateMetadata ํ•จ์ˆ˜ ๋‚ด๋ถ€) import { Metadata } from 'next'; export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> { const post = await getPostData(params.slug); // ๊ฐ€์ƒ์˜ ํฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ ํŽ˜์นญ if (!post) return { title: 'Post Not Found' }; return { title: post.title, description: post.summary, openGraph: { title: post.title, description: post.summary, url: `https://blue.dev/posts/${params.slug}`, siteName: '๋ธ”๋ฃจ์˜ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ', images: [ { url: post.thumbnailUrl, width: 1200, height: 630, alt: post.title, }, ], locale: 'ko_KR', type: 'article', }, twitter: { card: 'summary_large_image', title: post.title, description: post.summary, creator: '@blue_dev', images: [post.thumbnailUrl], }, }; } // ... (๋‚˜๋จธ์ง€ ์ฝ”๋“œ) async function getPostData(slug: string) { return { title: `Next.js ${slug} ์‹ฌํ™” ๊ฐ€์ด๋“œ`, summary: `์ด๊ฒƒ์€ ${slug}์— ๋Œ€ํ•œ ์‹ฌ๋„ ์žˆ๋Š” ๊ฐ€์ด๋“œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. Next.js ๊ฐœ๋ฐœ์ž๋“ค์ด ์•Œ์•„์•ผ ํ•  ๋ชจ๋“  ๊ฒƒ์„ ๋‹ด์•˜์–ด์š”.`, thumbnailUrl: `https://blue.dev/images/${slug}-og.jpg`, }; }

images ์†์„ฑ์€ ๋ฐฐ์—ด ํ˜•ํƒœ๋กœ ์—ฌ๋Ÿฌ ์ด๋ฏธ์ง€๋ฅผ ์ง€์ •ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ์ด๋ฏธ์ง€์— width, height, alt ์†์„ฑ์„ ๋ถ€์—ฌํ•˜์—ฌ ์ƒ์„ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด์š”.
ํŠนํžˆ summary_large_image ํƒ€์ž…์˜ Twitter Card๋Š” ํฐ ์ด๋ฏธ์ง€๋ฅผ ๋ณด์—ฌ์ฃผ๋ฏ€๋กœ ํด๋ฆญ๋ฅ ์„ ๋†’์ด๋Š” ๋ฐ ํšจ๊ณผ์ ์ด์—์š”.

2๏ธโƒฃ alternates๋กœ ๋‹ค๊ตญ์–ด ๋ฐ ์บ๋…ธ๋‹ˆ์ปฌ ์„ค์ •

๋‹ค๊ตญ์–ด ์›น์‚ฌ์ดํŠธ๋ฅผ ์šด์˜ํ•˜๊ฑฐ๋‚˜, ๋™์ผํ•œ ์ฝ˜ํ…์ธ ๊ฐ€ ์—ฌ๋Ÿฌ URL๋กœ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ alternates ์†์„ฑ์ด ์ค‘์š”ํ•ด์š”.
์ด๋Š” ๊ฒ€์ƒ‰ ์—”์ง„์— ์–ด๋–ค URL์ด ํ•ด๋‹น ์ฝ˜ํ…์ธ ์˜ "์ •์‹" ๋ฒ„์ „์ธ์ง€, ๋˜๋Š” ์–ด๋–ค ์–ธ์–ด ๋ฒ„์ „์„ ์ œ๊ณตํ•˜๋Š”์ง€ ์•Œ๋ ค์ค˜์š”.

// app/products/[id]/page.tsx (generateMetadata ํ•จ์ˆ˜ ๋‚ด๋ถ€) import { Metadata } from 'next'; export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> { const product = await getProductData(params.id); if (!product) return { title: 'Product Not Found' }; return { title: product.name, description: product.description, alternates: { canonical: `https://blue.dev/products/${params.id}`, // ์ •์‹ URL ์ง€์ • languages: { 'en-US': `https://blue.dev/en-US/products/${params.id}`, 'ja-JP': `https://blue.dev/ja-JP/products/${params.id}`, 'x-default': `https://blue.dev/products/${params.id}`, // ๊ธฐ๋ณธ ์–ธ์–ด }, }, }; } // ... (๋‚˜๋จธ์ง€ ์ฝ”๋“œ) async function getProductData(id: string) { return { name: `๋ธ”๋ฃจ์˜ ํ”„๋ฆฌ๋ฏธ์—„ ์ƒํ’ˆ ${id}`, description: `์ด ์ƒํ’ˆ์€ ${id}๋ฒˆ ์ƒํ’ˆ์œผ๋กœ, ์ตœ๊ณ ์˜ ํ’ˆ์งˆ์„ ์ž๋ž‘ํ•ด์š”.`, }; }

canonical์€ ์ค‘๋ณต ์ฝ˜ํ…์ธ  ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ณ  SEO ์ ์ˆ˜๋ฅผ ์˜ฌ๋ฆฌ๋Š” ๋ฐ ํ•„์ˆ˜์ ์ด๊ณ ์š”.
languages๋Š” ๋‹ค๊ตญ์–ด ๋ฒ„์ „์ด ์žˆ๋Š” ๊ฒฝ์šฐ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์‚ฌ์šฉ์ž ์–ธ์–ด์— ๋งž๋Š” ํŽ˜์ด์ง€๋ฅผ ์ œ๊ณตํ•˜๋„๋ก ๋•๋Š” ์—ญํ• ์„ ํ•ด์š”.

3๏ธโƒฃ ์ƒ์† ๋ฐ ์˜ค๋ฒ„๋ผ์ด๋”ฉ ์›๋ฆฌ ์ดํ•ด

generateMetadata๋Š” ๊ณ„์ธต์ ์œผ๋กœ ๋™์ž‘ํ•ด์š”.
app/layout.tsx์—์„œ ์ •์˜๋œ metadata๋Š” ๋ชจ๋“  ํ•˜์œ„ ๋ผ์šฐํŠธ์— ์ƒ์†๋ผ์š”.
๊ทธ๋ฆฌ๊ณ  ํ•˜์œ„ ๋ผ์šฐํŠธ(app/blog/layout.tsx๋‚˜ app/blog/posts/[slug]/page.tsx)์—์„œ generateMetadata๋ฅผ ์ •์˜ํ•˜๋ฉด, ๋ถ€๋ชจ์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํ™•์žฅํ•˜๊ฑฐ๋‚˜ ํŠน์ • ์†์„ฑ์„ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
์ด๋•Œ, ๊ฐ์ฒด ๋ณ‘ํ•ฉ(Object Merge) ๋ฐฉ์‹์œผ๋กœ ๋™์ž‘ํ•˜๋ฉฐ, ์ž์‹์—์„œ ์ •์˜๋œ ์†์„ฑ์ด ๋ถ€๋ชจ์˜ ์†์„ฑ์„ ๋ฎ์–ด์”Œ์šฐ๋Š” ๋ฐฉ์‹์ด์—์š”.
์˜ˆ๋ฅผ ๋“ค์–ด, layout.tsx์— title์ด ์ •์˜๋˜์–ด ์žˆ๊ณ , page.tsx์—๋„ title์ด ์ •์˜๋˜์–ด ์žˆ๋‹ค๋ฉด page.tsx์˜ title์ด ์ตœ์ข…์ ์œผ๋กœ ์ ์šฉ๋œ๋‹ต๋‹ˆ๋‹ค.
ํ•˜์ง€๋งŒ openGraph.images ๊ฐ™์€ ๋ฐฐ์—ด ํƒ€์ž…์€ ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ณ‘ํ•ฉ๋˜์ง€ ์•Š๊ณ  ๋ฎ์–ด์”Œ์›Œ์ง€๋ฏ€๋กœ ์ฃผ์˜ํ•ด์•ผ ํ•ด์š”.
Next.js 14๋ถ€ํ„ฐ๋Š” metadata.template์„ ์‚ฌ์šฉํ•˜์—ฌ ์ƒ์†๋œ title์— ์ ‘๋‘์‚ฌ/์ ‘๋ฏธ์‚ฌ๋ฅผ ์‰ฝ๊ฒŒ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/layout.tsx export const metadata = { title: { template: '%s | ๋ธ”๋ฃจ์˜ ๋ธ”๋กœ๊ทธ', // ์ž์‹ ํŽ˜์ด์ง€์˜ title์ด %s์— ๋“ค์–ด์™€์š”. default: '๋ธ”๋ฃจ์˜ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ', // title์ด ์—†๋Š” ํŽ˜์ด์ง€์˜ ๊ธฐ๋ณธ๊ฐ’ }, description: '๋ธ”๋ฃจ์˜ ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.', }; // app/posts/[slug]/page.tsx export async function generateMetadata({ params }: { params: { slug: string } }) { const post = await getPostData(params.slug); return { title: post.title, // 'Next.js ์‹ฌํ™” ๊ฐ€์ด๋“œ | ๋ธ”๋ฃจ์˜ ๋ธ”๋กœ๊ทธ'๋กœ ๋ Œ๋”๋ง๋ผ์š”. }; } // ...

์ด๋ ‡๊ฒŒ template์„ ํ™œ์šฉํ•˜๋ฉด ๋ชจ๋“  ํŽ˜์ด์ง€์— ์ผ๊ด€๋œ ์ œ๋ชฉ ํ˜•์‹์„ ์œ ์ง€ํ•˜๋ฉด์„œ๋„ ๋™์ ์ธ ์ œ๋ชฉ์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์–ด์š”.

generateViewport ํ•จ์ˆ˜๋Š” viewport ๋ฉ”ํƒ€ํƒœ๊ทธ์™€ ๊ด€๋ จ๋œ ์„ค์ •์„ ์ฒ˜๋ฆฌํ•˜๋ฉฐ, ์ฃผ๋กœ ๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ๊ณผ PWA ๊ฒฝํ—˜์— ์˜ํ–ฅ์„ ์ค˜์š”.
์ด ํ•จ์ˆ˜๋„ generateMetadata์™€ ์œ ์‚ฌํ•˜๊ฒŒ layout.tsx๋‚˜ page.tsx์—์„œ ์ •์˜ํ•  ์ˆ˜ ์žˆ์–ด์š”.

0๏ธโƒฃ viewport ๋ฉ”ํƒ€ํƒœ๊ทธ์˜ ์—ญํ• 

viewport ๋ฉ”ํƒ€ํƒœ๊ทธ๋Š” ์›นํŽ˜์ด์ง€๊ฐ€ ๋‹ค์–‘ํ•œ ๊ธฐ๊ธฐ์—์„œ ์–ด๋–ป๊ฒŒ ๋ Œ๋”๋ง๋ ์ง€๋ฅผ ๋ธŒ๋ผ์šฐ์ €์— ์•Œ๋ ค์ฃผ๋Š” ์—ญํ• ์„ ํ•ด์š”.
๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์—์„œ ์›นํŽ˜์ด์ง€๊ฐ€ ์ถ•์†Œ๋˜์ง€ ์•Š๊ณ  ๋ฐ˜์‘ํ˜•์œผ๋กœ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ‘œ์‹œ๋˜๋„๋ก ํ•˜๋Š” ๋ฐ ํ•„์ˆ˜์ ์ด์ฃ .
์˜ˆ๋ฅผ ๋“ค์–ด, width=device-width, initial-scale=1.0์€ ๊ธฐ๊ธฐ ๋„ˆ๋น„์— ๋งž์ถฐ ํŽ˜์ด์ง€ ๋„ˆ๋น„๋ฅผ ์„ค์ •ํ•˜๊ณ  ์ดˆ๊ธฐ ํ™•๋Œ€/์ถ•์†Œ ๋น„์œจ์„ 1.0์œผ๋กœ ์ง€์ •ํ•˜๋Š” ๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์„ค์ •์ด์—์š”.

1๏ธโƒฃ ๋‹คํฌ ๋ชจ๋“œ, ํ…Œ๋งˆ ์ƒ‰์ƒ ๋“ฑ ๊ณ ๊ธ‰ ์„ค์ •

generateViewport๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด PWA์˜ ์„ค์น˜ ๋ฐฐ๋„ˆ, ์ฃผ์†Œ ํ‘œ์‹œ์ค„ ์ƒ‰์ƒ ๋“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ–ฅ์ƒ์‹œํ‚ค๋Š” ๋‹ค์–‘ํ•œ ์„ค์ •์„ ํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/layout.tsx import { Viewport } from 'next'; export const viewport: Viewport = { width: 'device-width', initialScale: 1, maximumScale: 1, userScalable: false, // ์‚ฌ์šฉ์ž์˜ ํ™•๋Œ€/์ถ•์†Œ ๋น„ํ™œ์„ฑํ™” (์„ ํƒ ์‚ฌํ•ญ) themeColor: '#0070f3', // PWA ํ…Œ๋งˆ ์ƒ‰์ƒ (์•ฑ ๋ฐ” ์ƒ‰์ƒ ๋“ฑ) colorScheme: 'dark light', // ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ์ง€์› }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body>{children}</body> </html> ); }

์—ฌ๊ธฐ์„œ themeColor๋Š” PWA๊ฐ€ ์„ค์น˜๋  ๋•Œ ์•ฑ์˜ ๊ธฐ๋ณธ ์ƒ‰์ƒ์„ ์ง€์ •ํ•˜๊ณ , ๋ชจ๋ฐ”์ผ ๋ธŒ๋ผ์šฐ์ €์˜ ์ฃผ์†Œ ํ‘œ์‹œ์ค„ ์ƒ‰์ƒ์—๋„ ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์–ด์š”.
colorScheme: 'dark light'๋Š” ์›น์‚ฌ์ดํŠธ๊ฐ€ ๋‹คํฌ ๋ชจ๋“œ์™€ ๋ผ์ดํŠธ ๋ชจ๋“œ๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•จ์„ ๋ธŒ๋ผ์šฐ์ €์— ์•Œ๋ ค์ฃผ์–ด, ์‚ฌ์šฉ์ž ๊ธฐ๊ธฐ์˜ ์„ค์ •์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ๋ชจ๋“œ๋ฅผ ์ ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š”๋‹ต๋‹ˆ๋‹ค.

๐Ÿงช ์‹ค์ „ ์ฝ”๋“œ ์˜ˆ์‹œ

์ด๋ฒˆ ์„น์…˜์—์„œ๋Š” ์‹ค์ œ ํ”„๋กœ์ ํŠธ์—์„œ generateMetadata์™€ generateViewport๋ฅผ ์–ด๋–ป๊ฒŒ ์กฐํ•ฉํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”.

0๏ธโƒฃ ๋ ˆ์ด์•„์›ƒ์—์„œ ๊ณตํ†ต ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ •

๋จผ์ €, app/layout.tsx์—์„œ ์›น์‚ฌ์ดํŠธ ์ „๋ฐ˜์— ๊ฑธ์ณ ์ ์šฉ๋  ๊ธฐ๋ณธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ๋ทฐํฌํŠธ ์„ค์ •์„ ์ •์˜ํ•ด์š”.
์ด๋Š” ๋ชจ๋“  ํŽ˜์ด์ง€์— ๊ณตํ†ต์ ์œผ๋กœ ์ ์šฉ๋˜๋Š” SEO ๊ธฐ๋ณธ๊ฐ’๊ณผ PWA ํ…Œ๋งˆ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐ ์œ ์šฉํ•ด์š”.

// app/layout.tsx import { Metadata, Viewport } from 'next'; import './globals.css'; // ์ „์—ญ ์Šคํƒ€์ผ์‹œํŠธ export const metadata: Metadata = { metadataBase: new URL('https://blue.dev'), // ์ •์‹ ๋„๋ฉ”์ธ ์„ค์ • title: { template: '%s | ๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ', default: '๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ: ์‹ค์ „ ์›น ๊ฐœ๋ฐœ ๊ฐ€์ด๋“œ', }, description: '์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ๊ฐ€ ๊ณต์œ ํ•˜๋Š” Next.js, React, TypeScript, ์›น ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋…ธํ•˜์šฐ.', keywords: ['Next.js', 'React', 'TypeScript', '์›น ๊ฐœ๋ฐœ', 'SEO', 'PWA', 'ํ”„๋ก ํŠธ์—”๋“œ'], openGraph: { title: '๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ', description: '์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ๊ฐ€ ๊ณต์œ ํ•˜๋Š” Next.js, React, TypeScript, ์›น ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋…ธํ•˜์šฐ.', url: 'https://blue.dev', siteName: '๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ', images: [ { url: 'https://blue.dev/og-image.jpg', // ๊ธฐ๋ณธ OG ์ด๋ฏธ์ง€ width: 1200, height: 630, alt: '๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ', }, ], locale: 'ko_KR', type: 'website', }, twitter: { card: 'summary_large_image', title: '๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ', description: '์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž ๋ธ”๋ฃจ๊ฐ€ ๊ณต์œ ํ•˜๋Š” Next.js, React, TypeScript, ์›น ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋…ธํ•˜์šฐ.', images: ['https://blue.dev/twitter-image.jpg'], creator: '@blue_dev', }, }; export const viewport: Viewport = { width: 'device-width', initialScale: 1, themeColor: '#1a202c', // ๋‹คํฌํ•œ ํ…Œ๋งˆ ์ƒ‰์ƒ colorScheme: 'dark light', }; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body>{children}</body> </html> ); }

metadataBase๋ฅผ ์„ค์ •ํ•˜๋ฉด ๋ชจ๋“  ์ƒ๋Œ€ ๊ฒฝ๋กœ URL(์˜ˆ: images ์†์„ฑ)์ด ์ด ๊ธฐ๋ณธ URL์„ ๊ธฐ์ค€์œผ๋กœ ํ•ด์„๋˜์–ด SEO์— ์œ ๋ฆฌํ•ด์š”.
title.template๊ณผ title.default๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ํŽ˜์ด์ง€ ์ œ๋ชฉ ๊ด€๋ฆฌ๊ฐ€ ํ›จ์”ฌ ํŽธ๋ฆฌํ•ด์ง„๋‹ต๋‹ˆ๋‹ค.

1๏ธโƒฃ ํŽ˜์ด์ง€์—์„œ ๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์˜ค๋ฒ„๋ผ์ด๋”ฉ

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

// app/blog/[slug]/page.tsx import { Metadata } from 'next'; type PostPageProps = { params: { slug: string }; }; // ๊ฐ€์ƒ์˜ ํฌ์ŠคํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ํ•จ์ˆ˜ async function getPostData(slug: string) { // ์‹ค์ œ API ํ˜ธ์ถœ ๋˜๋Š” DB ์ฟผ๋ฆฌ ๋กœ์ง์ด ๋“ค์–ด๊ฐˆ ๊ณณ์ด์—์š”. // ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋ฅผ ์œ„ํ•ด ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”ใ€‚ await new Promise(resolve => setTimeout(resolve, 100)); // ๋น„๋™๊ธฐ ์ž‘์—… ์‹œ๋ฎฌ๋ ˆ์ด์…˜ return { id: slug, title: `Next.js App Router์˜ ${slug} ์™„๋ฒฝ ๊ฐ€์ด๋“œ`, content: `์ด๊ฒƒ์€ ${slug}์— ๋Œ€ํ•œ ๋งค์šฐ ์ƒ์„ธํ•œ ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค. ๊ฐœ๋ฐœ์ž๋“ค์ด ๊ถ๊ธˆํ•ดํ•˜๋Š” ๋ชจ๋“  ๊ฒƒ์„ ๋‹ด์•˜์–ด์š”.`, description: `Next.js App Router์—์„œ ${slug}๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ณด์„ธ์š”.`, imageUrl: `https://blue.dev/blog/${slug}-hero.jpg`, publishedDate: '2026-05-22', }; } export async function generateMetadata( { params }: PostPageProps, ): Promise<Metadata> { const post = await getPostData(params.slug); return { title: post.title, // 'Next.js App Router์˜ [slug] ์™„๋ฒฝ ๊ฐ€์ด๋“œ | ๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ'๋กœ ๋ Œ๋”๋ง description: post.description, openGraph: { images: [post.imageUrl], // ์ƒˆ๋กœ์šด OG ์ด๋ฏธ์ง€๋กœ ์˜ค๋ฒ„๋ผ์ด๋”ฉ }, }; } export default async function PostPage({ params }: PostPageProps) { const post = await getPostData(params.slug); // ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•ด์š”ใ€‚ return ( <article> <h1>{post.title}</h1> <img src={post.imageUrl} alt={post.title} style={{ maxWidth: '100%', height: 'auto' }} /> <p>{post.content}</p> <small>๊ฒŒ์‹œ์ผ: {post.publishedDate}</small> </article> ); }

์—ฌ๊ธฐ์„œ generateMetadata ํ•จ์ˆ˜๋Š” post ๋ฐ์ดํ„ฐ๋ฅผ ๋น„๋™๊ธฐ๋กœ ๊ฐ€์ ธ์™€์„œ ํŽ˜์ด์ง€ ์ œ๋ชฉ, ์„ค๋ช…, Open Graph ์ด๋ฏธ์ง€๋ฅผ ๋™์ ์œผ๋กœ ์„ค์ •ํ•˜๊ณ  ์žˆ์–ด์š”.
layout.tsx์—์„œ ์ •์˜๋œ title.template ๋•๋ถ„์— post.title๋งŒ ์ง€์ •ํ•ด๋„ ์ตœ์ข… ์ œ๋ชฉ์€ "Next.js App Router์˜ [slug] ์™„๋ฒฝ ๊ฐ€์ด๋“œ | ๋ธ”๋ฃจ์˜ ๊ฐœ๋ฐœ ๋ธ”๋กœ๊ทธ"์™€ ๊ฐ™์ด ์ž๋™์œผ๋กœ ์™„์„ฑ๋ผ์š”.
openGraph.images๋Š” ๋ฐฐ์—ด์ด๋ฏ€๋กœ, layout.tsx์˜ ๊ธฐ๋ณธ ์ด๋ฏธ์ง€๋ฅผ ์ด ํŽ˜์ด์ง€์˜ ํŠน์ • ์ด๋ฏธ์ง€๋กœ ๋ฎ์–ด์”Œ์šฐ๊ฒŒ ๋œ๋‹ต๋‹ˆ๋‹ค.

๐Ÿ“ ์ •๋ฆฌ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ ๋ฐ ํ™œ์šฉ ํŒ

์˜ค๋Š˜์€ Next.js App Router์—์„œ generateMetadata์™€ generateViewport๋ฅผ ํ™œ์šฉํ•˜์—ฌ SEO์™€ PWA๋ฅผ ์ตœ์ ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์‹ฌ๋„ ์žˆ๊ฒŒ ์‚ดํŽด๋ณด์•˜์–ด์š”.
ํ•ต์‹ฌ ๋‚ด์šฉ์„ ๋‹ค์‹œ ํ•œ๋ฒˆ ์ •๋ฆฌํ•ด ๋“œ๋ฆด๊ฒŒ์š”.


์œ ์šฉํ•œ ํŒ

generateMetadata: ์›น ํŽ˜์ด์ง€์˜ ์ œ๋ชฉ, ์„ค๋ช…, ํ‚ค์›Œ๋“œ, Open Graph, Twitter Card ๋“ฑ์„ ๋™์ ์œผ๋กœ ์„ค์ •ํ•˜์—ฌ ๊ฒ€์ƒ‰ ์—”์ง„ ๋…ธ์ถœ๊ณผ ์†Œ์…œ ๊ณต์œ  ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ด์š”.
๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์—์„œ ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•˜๊ณ , ์ž์‹ ํŽ˜์ด์ง€์—์„œ ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ ์˜ค๋ฒ„๋ผ์ด๋”ฉํ•˜๋Š” ๊ณ„์ธต์  ๊ตฌ์กฐ๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.
ํŠนํžˆ metadataBase์™€ title.template์€ SEO ๊ด€๋ฆฌ์˜ ํšจ์œจ์„ฑ์„ ๋†’์—ฌ์ค€๋‹ต๋‹ˆ๋‹ค.


์œ ์šฉํ•œ ํŒ

generateViewport: ๋ชจ๋ฐ”์ผ ๊ธฐ๊ธฐ์—์„œ์˜ ๋ฐ˜์‘ํ˜• ๋™์ž‘, PWA ํ…Œ๋งˆ ์ƒ‰์ƒ, ๋‹คํฌ/๋ผ์ดํŠธ ๋ชจ๋“œ ์ง€์› ๋“ฑ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ์ง์ ‘์ ์ธ ์˜ํ–ฅ์„ ์ฃผ๋Š” ๋ทฐํฌํŠธ ๊ด€๋ จ ์„ค์ •์„ ๋‹ด๋‹นํ•ด์š”.
themeColor์™€ colorScheme์„ ์ ์ ˆํžˆ ์„ค์ •ํ•˜์—ฌ PWA์˜ ์•ฑ ๊ฐ™์€ ๊ฒฝํ—˜์„ ๊ฐ•ํ™”ํ•  ์ˆ˜ ์žˆ์–ด์š”.


์ด ๋‘ ํ•จ์ˆ˜๋Š” Next.js App Router์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ ์ค‘ ํ•˜๋‚˜์ด๋ฉฐ, ์›น์‚ฌ์ดํŠธ์˜ ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”์™€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๋™์‹œ์— ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ๋Š” ์ค‘์š”ํ•œ ๋„๊ตฌ์˜ˆ์š”.
์‹ค๋ฌด์—์„œ ์ด ๊ธฐ๋Šฅ๋“ค์„ ์ ๊ทน์ ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ๋” ๋‚˜์€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋งŒ๋“ค์–ด๋ณด์‹œ๊ธธ ๋ฐ”๋ผ์š”!

1๏ธโƒฃ ๋‹ค์Œ ๋‹จ๊ณ„: ๋” ๋‚˜์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์œ„ํ•ด

generateMetadata์™€ generateViewport ์™ธ์—๋„ Next.js App Router๋Š” ์›น ์„ฑ๋Šฅ๊ณผ ๊ฐœ๋ฐœ ํŽธ์˜์„ฑ์„ ์œ„ํ•œ ๋‹ค์–‘ํ•œ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ด์š”.
์˜ˆ๋ฅผ ๋“ค์–ด, robots.txt์™€ sitemap.xml์„ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•,

๋“ฑ์„ ์ถ”๊ฐ€์ ์œผ๋กœ ํ•™์Šตํ•ด ๋ณด์‹œ๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์•„์š”.
๋˜ํ•œ, Core Web Vitals ์ ์ˆ˜๋ฅผ ๊ฐœ์„ ํ•˜๊ธฐ ์œ„ํ•œ ์ด๋ฏธ์ง€ ์ตœ์ ํ™”, ํฐํŠธ ์ตœ์ ํ™”, ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ค„์ด๊ธฐ ๋“ฑ๋„ ๊พธ์ค€ํžˆ ๊ด€์‹ฌ์„ ๊ฐ€์ ธ์•ผ ํ•  ๋ถ€๋ถ„์ด์—์š”.
์ง€์†์ ์ธ ํ•™์Šต๊ณผ ์ ์šฉ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์—๊ฒŒ ์ตœ๊ณ ์˜ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•˜๋Š” ์›น ์„œ๋น„์Šค๋ฅผ ๋งŒ๋“ค์–ด๋‚˜๊ฐ€์‹œ๊ธธ ์‘์›ํ•ด์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ