๐Ÿš€ Next.js 11์žฅ: Metadata API & SEO โ€” ๊ฒ€์ƒ‰์—”์ง„์ด ์‚ฌ๋ž‘ํ•˜๋Š” ์•ฑ ๋งŒ๋“ค๊ธฐ

๐Ÿ“‹ ๊ฐœ์š”

Metadata API๋กœ OG ํƒœ๊ทธ, ๊ตฌ์กฐํ™” ๋ฐ์ดํ„ฐ, sitemap์„ ์„ค์ •ํ•ด ๊ฒ€์ƒ‰์—”์ง„์— ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„ (์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 7๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์‹ ์ž…): "์–ด? ์šฐ๋ฆฌ ์„œ๋น„์Šค ์นด์นด์˜คํ†ก์œผ๋กœ ๋งํฌ ๊ณต์œ ํ–ˆ๋”๋‹ˆ ์ธ๋„ค์ผ์ด ์•ˆ ๋œจ๊ณ  URL๋งŒ ๋ฉ๊ทธ๋Ÿฌ๋‹ˆ ๋‚˜์˜ค๋„ค์š”. ๊ทธ๋ฆฌ๊ณ  ๊ตฌ๊ธ€์— ๊ฒ€์ƒ‰ํ•ด๋„ ์šฐ๋ฆฌ ์„œ๋น„์Šค ์ด๋ฆ„์ด ์•ˆ ๋‚˜์˜ค๊ณ  'Vercel App'์ด๋ผ๊ณ ๋งŒ ๋œจ๋Š”๋ฐ... ์–ด๋–ป๊ฒŒ ๊ณ ์ณ์š”?"
  • ์˜ํ˜ธ(๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜, <head> ํƒœ๊ทธ ์„ค์ •์„ ์•„์ง ์•ˆ ํ•˜์…จ๊ตฐ์š”. Next.js Metadata API ๋ฅผ ์“ฐ๋ฉด ํŒŒ์ผ ํ•˜๋‚˜ ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ ๋„ ๊ฐ ํŽ˜์ด์ง€๋ณ„ ํƒ€์ดํ‹€, ์„ค๋ช…, OG ์ด๋ฏธ์ง€๋ฅผ ์ •ํ™•ํžˆ ์„ธํŒ…ํ•  ์ˆ˜ ์žˆ์–ด์š”. SEO๋Š” ์„ ํƒ์ด ์•„๋‹ˆ๋ผ ์„œ๋น„์Šค ๊ฐ€์‹œ์„ฑ์˜ ๊ธฐ๋ณธ์ด์—์š”."

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
์ •์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ โ†’ ๋™์  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ(ํŽ˜์ด์ง€๋ณ„) โ†’ OG ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ โ†’ robots/sitemap ๊ณ ๊ธ‰ ์„ค์ •

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • layout.tsx์—์„œ ์‚ฌ์ดํŠธ ์ „์ฒด ๊ธฐ๋ณธ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์„ ์–ธํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ํŽ˜์ด์ง€์ฒ˜๋Ÿผ URL๋งˆ๋‹ค ๋‹ค๋ฅธ <title>, <description>์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค
  • ์นด์นด์˜คยท์Šฌ๋ž™ ๋งํฌ ๊ณต์œ  ์‹œ ์˜ˆ์œ ์ธ๋„ค์ผ์ด ๋œจ๋Š” OG ์ด๋ฏธ์ง€๋ฅผ JSX ์ฝ”๋“œ๋กœ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

๋ฆฌ์•กํŠธ SPA(Single Page Application)์˜ ๊ฐ€์žฅ ํฐ ๋‹จ์ ์ด ๋ญ”์ง€ ์•Œ์•„? ๋ฐ”๋กœ SEO(๊ฒ€์ƒ‰์—”์ง„ ์ตœ์ ํ™”) ์˜€์–ด.

SPA๋Š” ์ฒ˜์Œ ์„œ๋ฒ„์—์„œ ๋นˆ HTML์„ ๋‚ด๋ ค๋ณด๋‚ด๊ณ , JavaScript๊ฐ€ ๋กœ๋“œ๋œ ๋’ค์—์•ผ ํ™”๋ฉด์„ ์ฑ„์›Œ. ๊ทธ๋Ÿฐ๋ฐ ๊ตฌ๊ธ€ ๋ด‡์ด ํŽ˜์ด์ง€๋ฅผ ํฌ๋กค๋งํ•˜๋Ÿฌ ์™”์„ ๋•Œ, JS๊ฐ€ ์‹คํ–‰๋˜๊ธฐ ์ „์ด๋ผ๋ฉด ๋นˆ ๊ป๋ฐ๊ธฐ๋งŒ ๋ณด๊ฒŒ ๋ผ.

๊ทธ๋ž˜์„œ Next.js๊ฐ€ ๋“ฑ์žฅํ•œ ๊ฑฐ์•ผ. ์„œ๋ฒ„์—์„œ ์™„์„ฑ๋œ HTML์„ ๋‚ด๋ ค๋ณด๋‚ด๋‹ˆ๊นŒ ๋ด‡๋„ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ์ง€. ํ•˜์ง€๋งŒ ๊ฑฐ๊ธฐ์„œ ๋ฉˆ์ถ”๋ฉด ์•ˆ ๋ผ. ๋ด‡์ด "์ด ํŽ˜์ด์ง€๊ฐ€ ๋ญ”์ง€"๋ฅผ ์ •ํ™•ํžˆ ์•Œ๋ ค๋ฉด <title>, <description>, OG ํƒœ๊ทธ๋“ค์ด ์ œ๋Œ€๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•ด.

๊ณผ๊ฑฐ Pages Router ์‹œ์ ˆ์—” next/head๋กœ ๊ฐ ํŽ˜์ด์ง€๋งˆ๋‹ค <Head> ์ปดํฌ๋„ŒํŠธ๋ฅผ ์ˆ˜๋™์œผ๋กœ ๋‹ฌ์•„์คฌ์–ด. ์ค‘๋ณต ์ฝ”๋“œ๊ฐ€ ๋„˜์ณ๋‚ฌ๊ณ , ์ƒ์† ๊ฐœ๋…๋„ ์—†์–ด์„œ ๊ด€๋ฆฌ๊ฐ€ ์ง€์˜ฅ์ด์—ˆ์ง€. App Router์˜ Metadata API๋Š” ์ด ๋ชจ๋“  ๊ฑธ ํŒŒ์ผ ๊ธฐ๋ฐ˜ ์‹œ์Šคํ…œ์œผ๋กœ ํ†ตํ•ฉํ•ด๋ฒ„๋ ธ์–ด.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?
์ƒ์ ๋“ค์ด ๋ชจ์ธ ์‡ผํ•‘๋ชฐ์„ ์ƒ์ƒํ•ด๋ด. ๊ฐ ๊ฐ€๊ฒŒ ์•ž์—๋Š” ๊ฐ„ํŒ์ด ์žˆ์–ด.
๊ตฌ๊ธ€ ๋ด‡์€ ์ด ์‡ผํ•‘๋ชฐ์„ ์ˆœ์ฐฐํ•˜๋Š” ์•ˆ๋‚ด์›์ด์•ผ. ๊ฐ„ํŒ(๋ฉ”ํƒ€๋ฐ์ดํ„ฐ)์„ ์ฝ๊ณ  "์ด ๊ฐ€๊ฒŒ๋Š” ๋ญ˜ ํŒŒ๋Š” ๊ณณ์ด์—์š”"๋ผ๊ณ  ์ง€๋„(๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ)์— ๊ธฐ๋กํ•ด์ค˜.
๊ฐ„ํŒ์ด ์—†๊ฑฐ๋‚˜ "Vercel App"์ด๋ผ๊ณ ๋งŒ ์ ํ˜€ ์žˆ์œผ๋ฉด, ์•ˆ๋‚ด์›๋„ ๋ญ๋ผ๊ณ  ๊ธฐ๋กํ•ด์•ผ ํ• ์ง€ ๋ชฐ๋ผ์„œ ๊ทธ๋ƒฅ ์ฃผ์†Œ๋งŒ ์ ์–ด๋‘๋Š” ๊ฑฐ์•ผ.

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ํŽ˜์ด์ง€์˜ "์ •์ฒด์„ฑ ๋ช…ํ•จ"์ด์•ผ. layout.tsx๋Š” ๊ฑด๋ฌผ ์ „์ฒด ์•ˆ๋‚ดํŒ์ด๊ณ , page.tsx์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ๊ฐ ์ธต ๊ฐ„ํŒ์ธ ๊ฑฐ์ง€. ์ƒ์† ๊ตฌ์กฐ๊ฐ€ ์žˆ์–ด์„œ ์•„๋ž˜์ธต ๊ฐ„ํŒ์ด ์—†์œผ๋ฉด ์œ„์ธต ๊ฑด๋ฌผ ์•ˆ๋‚ดํŒ์œผ๋กœ ๋Œ€์ฒด๋ผ.


๐Ÿงฉ Static Metadata โ€” ๋ณ€ํ•˜์ง€ ์•Š๋Š” ๋ช…ํ•จ ๐ŸŸข

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • layout.tsx์—์„œ metadata ๊ฐ์ฒด๋ฅผ exportํ•ด ์‚ฌ์ดํŠธ ์ „์ฒด ๊ธฐ๋ณธ๊ฐ’์„ ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋‹ค
  • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ์ƒ์† ๊ตฌ์กฐ๊ฐ€ ์–ด๋–ป๊ฒŒ ์ž‘๋™ํ•˜๋Š”์ง€ ์„ค๋ช…ํ•  ์ˆ˜ ์žˆ๋‹ค

๊ฐ€์žฅ ๋‹จ์ˆœํ•œ ๋ฐฉ๋ฒ•๋ถ€ํ„ฐ ์‹œ์ž‘ํ•ด. ๊ทธ๋ƒฅ metadata ๊ฐ์ฒด๋ฅผ exportํ•˜๋ฉด ๋์ด์•ผ.

// app/layout.tsx โ€” ์‚ฌ์ดํŠธ ์ „์ฒด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
import type { Metadata } from 'next'
 
export const metadata: Metadata = {
  title: {
    // template: ํ•˜์œ„ ํŽ˜์ด์ง€์—์„œ title์„ ์„ค์ •ํ•˜๋ฉด ์ž๋™์œผ๋กœ " | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ" ๊ฐ€ ๋ถ™์Œ
    template: '%s | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ',
    // default: ํ•˜์œ„ ํŽ˜์ด์ง€์—์„œ title์„ ์„ค์ • ์•ˆ ํ•˜๋ฉด ์ด๊ฒŒ ๊ธฐ๋ณธ๊ฐ’
    default: '์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ โ€” ๊ฐœ๋ฐœ์ž ์Šคํ„ฐ๋”” ๋งค์นญ',
  },
  description: '๊ฐœ๋ฐœ์ž๋“ค์ด ๋ชจ์—ฌ ํ•จ๊ป˜ ์„ฑ์žฅํ•˜๋Š” ์Šคํ„ฐ๋”” ๋งค์นญ ํ”Œ๋žซํผ',
  // Open Graph: ์นด์นด์˜คํ†ก, ์Šฌ๋ž™, ํŽ˜์ด์Šค๋ถ ๋“ฑ์—์„œ ๋งํฌ ๊ณต์œ  ์‹œ ๋ฏธ๋ฆฌ๋ณด๊ธฐ ์นด๋“œ
  openGraph: {
    title: '์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ',
    description: '๊ฐœ๋ฐœ์ž ์Šคํ„ฐ๋”” ๋งค์นญ ํ”Œ๋žซํผ',
    url: 'https://youngsu.community',
    siteName: '์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ',
    images: [
      {
        url: 'https://youngsu.community/og-image.png',
        width: 1200,
        height: 630,
      },
    ],
    locale: 'ko_KR',
    type: 'website',
  },
}
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="ko">
      <body>{children}</body>
    </html>
  )
}

๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์ƒ์†(Merging) ๊ทœ์น™:

Next.js๋Š” ๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ๋ถ€ํ„ฐ ๊ฐ€์žฅ ๊นŠ์€ ํŽ˜์ด์ง€๊นŒ์ง€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๊นŠ์€ ๋ณ‘ํ•ฉ(deep merge) ํ•ด์ค˜. ๊ฐ™์€ ํ‚ค๊ฐ€ ์žˆ์œผ๋ฉด ์•„๋ž˜ ๊ณ„์ธต ๊ฐ’์ด ์ด๊ฒจ.

app/layout.tsx          โ†’ { title: { default: "์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ", template: "%s | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ" } }
  app/posts/layout.tsx  โ†’ { title: "๊ฒŒ์‹œํŒ" }       โ†’ ์ตœ์ข…: "๊ฒŒ์‹œํŒ | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ"
    app/posts/[id]/page.tsx โ†’ { title: "ํด๋กœ์ € ์Šคํ„ฐ๋””" } โ†’ ์ตœ์ข…: "ํด๋กœ์ € ์Šคํ„ฐ๋”” | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ"

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
layout.tsx์˜ metadata๋Š” ์œ ์ „์ž์•ผ. ์•„๋ž˜ ํŽ˜์ด์ง€๋Š” ๋ฎ์–ด์“ธ ์ˆ˜ ์žˆ๊ณ , ์„ค์ • ์•ˆ ํ•˜๋ฉด ๋ถ€๋ชจ ๊ฒƒ์„ ์ด์–ด๋ฐ›์•„.


๐ŸŒฑ Dynamic Metadata โ€” ํŽ˜์ด์ง€๋งˆ๋‹ค ๋‹ค๋ฅธ ์–ผ๊ตด ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • generateMetadata ํ•จ์ˆ˜๋กœ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ ๋™์ ์œผ๋กœ <title>, <description>์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค
  • DB์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ fetchํ•ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค” ์ž ๊น, ๋จผ์ € ์ƒ๊ฐํ•ด๋ด
๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ํŽ˜์ด์ง€ /posts/123์™€ /posts/456์€ ๋‹น์—ฐํžˆ ํƒ€์ดํ‹€์ด ๋‹ฌ๋ผ์•ผ ํ•ด.
์–ด๋–ป๊ฒŒ URL๋งˆ๋‹ค ๋‹ค๋ฅธ <title>์„ ์ž๋™์œผ๋กœ ๋„ฃ์„ ์ˆ˜ ์žˆ์„๊นŒ?

์ •์  metadata ๊ฐ์ฒด๋กœ๋Š” ํ•œ๊ณ„๊ฐ€ ์žˆ์–ด. ๊ฒŒ์‹œ๊ธ€ ID์— ๋”ฐ๋ผ ์ œ๋ชฉ์ด ๋‹ฌ๋ผ์ง€๋Š” ๋™์  ํŽ˜์ด์ง€์—์„  generateMetadata ํ•จ์ˆ˜๋ฅผ ์จ์•ผ ํ•ด.

// app/posts/[id]/page.tsx
 
import type { Metadata, ResolvingMetadata } from 'next'
 
// Props ํƒ€์ž…์€ page.tsx์˜ params์™€ ๋™์ผํ•˜๊ฒŒ
type Props = {
  params: Promise<{ id: string }>
}
 
// โœ… ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋กœ ์„ ์–ธ โ€” DB ์กฐํšŒ๋„ ๊ฐ€๋Šฅ
export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata     // ๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์— ์ ‘๊ทผ ๊ฐ€๋Šฅ
): Promise<Metadata> {
  const { id } = await params
 
  // DB์—์„œ ๊ฒŒ์‹œ๊ธ€ ๋ฐ์ดํ„ฐ fetch (Next.js๊ฐ€ ์ž๋™์œผ๋กœ ์บ์‹ฑํ•ด์คŒ โ€” 6์žฅ ์ฐธ๊ณ )
  const post = await fetch(`https://api.youngsu.community/posts/${id}`, {
    next: { revalidate: 3600 },  // 1์‹œ๊ฐ„ ์บ์‹ฑ
  }).then((res) => res.json())
 
  // ๋ถ€๋ชจ ๋ ˆ์ด์•„์›ƒ์˜ OG ์ด๋ฏธ์ง€ ๋ฐฐ์—ด์— ๋‚ด ์ด๋ฏธ์ง€ ์ถ”๊ฐ€ํ•˜๋Š” ํŒจํ„ด
  const previousImages = (await parent).openGraph?.images || []
 
  return {
    title: post.title,                          // โ†’ "ํด๋กœ์ € ์Šคํ„ฐ๋”” | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ"
    description: post.summary,                 // ๊ฒŒ์‹œ๊ธ€ ์š”์•ฝ
    openGraph: {
      title: post.title,
      description: post.summary,
      images: [
        `/posts/${id}/opengraph-image`,         // ๋™์  OG ์ด๋ฏธ์ง€ (๋‹ค์Œ ์„น์…˜)
        ...previousImages,                      // ๋ถ€๋ชจ ์ด๋ฏธ์ง€๋„ fallback์œผ๋กœ ์œ ์ง€
      ],
    },
  }
}
 
export default async function PostPage({ params }: Props) {
  const { id } = await params
  // ...ํŽ˜์ด์ง€ ์ปดํฌ๋„ŒํŠธ
}

์˜์ฒ ์ด์˜ ์‹ค์ˆ˜ ํŒจํ„ด:

// โŒ ์˜์ฒ ์ด์˜ ์ˆœ์ง„ํ•œ ์ฝ”๋“œ โ€” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ •ํ•˜๋ ค๋Š” ์‹œ๋„
'use client'
import { useEffect } from 'react'
 
export default function PostPage() {
  useEffect(() => {
    // ์ด๊ฑด ์ ˆ๋Œ€ ์•ˆ ๋ผ! ๋ด‡์€ JS ์‹คํ–‰ ์ „์— HTML์„ ์ฝ์–ด๊ฐ€๊ฑฐ๋“ 
    document.title = 'ํด๋กœ์ € ์Šคํ„ฐ๋””'
  }, [])
  return <div>...</div>
}
 
// โœ… ์˜ํ˜ธ๊ฐ€ ๊ณ ์ณ์ค€ ์ฝ”๋“œ
// โ†’ ์œ„์˜ generateMetadata ํ•จ์ˆ˜ ์‚ฌ์šฉ (์„œ๋ฒ„์—์„œ ์‹คํ–‰๋˜๋ฏ€๋กœ ๋ด‡๋„ ์ฝ์„ ์ˆ˜ ์žˆ์Œ)

โžก๏ธ ๋‹ค์Œ ์„น์…˜์—์„œ๋Š”: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์˜ ๊ฝƒ, OG ์ด๋ฏธ์ง€๋ฅผ ์ฝ”๋“œ๋กœ ์ž๋™ ์ƒ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ด.


๐Ÿ–ผ๏ธ OG ์ด๋ฏธ์ง€ ์ž๋™ ์ƒ์„ฑ โ€” opengraph-image.tsx ๐ŸŸก

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • opengraph-image.tsx ํŒŒ์ผ ํ•˜๋‚˜๋กœ JSX๋ฅผ ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•˜๋Š” Dynamic OG Image๋ฅผ ๋งŒ๋“ค ์ˆ˜ ์žˆ๋‹ค
  • URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ ๊ฒŒ์‹œ๊ธ€๋งˆ๋‹ค ๋‹ค๋ฅธ OG ์ด๋ฏธ์ง€๋ฅผ ์ž๋™ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค

์นด์นด์˜คํ†ก์ด๋‚˜ ์Šฌ๋ž™์—์„œ ๋งํฌ๋ฅผ ๊ณต์œ ํ•  ๋•Œ ์˜ˆ์œ ์ธ๋„ค์ผ์ด ๋œจ๋Š” ๊ฑฐ ๋ดค์ง€? ๊ทธ๊ฒŒ OG(Open Graph) ์ด๋ฏธ์ง€์•ผ.

Next.js๋Š” opengraph-image.tsx๋ผ๋Š” ํŠน์ˆ˜ ํŒŒ์ผ์„ ํ†ตํ•ด JSX๋กœ ์ด๋ฏธ์ง€๋ฅผ ๋™์  ์ƒ์„ฑํ•˜๋Š” ๊ธฐ๋Šฅ์„ ๋‚ด์žฅํ•˜๊ณ  ์žˆ์–ด. ๋‚ด๋ถ€์ ์œผ๋กœ๋Š” @vercel/og ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Satori ์—”์ง„์ด JSX๋ฅผ SVG โ†’ PNG๋กœ ๋ณ€ํ™˜ํ•ด์ค˜.

// app/posts/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
 
// ์ด๋ฏธ์ง€ ํฌ๊ธฐ ์„ค์ • (๊ถŒ์žฅ: 1200x630)
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
 
// params๋กœ URL ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ๋ฐ›์•„ ๋™์  ์ด๋ฏธ์ง€ ์ƒ์„ฑ
export default async function Image({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params
 
  // DB์—์„œ ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ ๊ฐ€์ ธ์˜ค๊ธฐ (์„œ๋ฒ„์—์„œ ์‹คํ–‰)
  const post = await fetch(`https://api.youngsu.community/posts/${id}`).then((r) => r.json())
 
  return new ImageResponse(
    // JSX๋กœ ์ด๋ฏธ์ง€ ๋ ˆ์ด์•„์›ƒ ๊ตฌ์„ฑ
    <div
      style={{
        display: 'flex',
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'center',
        width: '100%',
        height: '100%',
        backgroundColor: '#0F172A',           // ๋‹คํฌ ๋ฐฐ๊ฒฝ
        padding: '60px',
      }}
    >
      {/* ์ปค๋ฎค๋‹ˆํ‹ฐ ๋กœ๊ณ  */}
      <div style={{ color: '#60A5FA', fontSize: 28, marginBottom: 24 }}>
        ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ
      </div>
      {/* ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ โ€” DB์—์„œ ๊ฐ€์ ธ์˜จ ๋™์  ๋ฐ์ดํ„ฐ */}
      <div
        style={{
          color: '#F1F5F9',
          fontSize: 56,
          fontWeight: 'bold',
          textAlign: 'center',
          lineHeight: 1.3,
        }}
      >
        {post.title}
      </div>
    </div>,
    { ...size }
  )
}

๊ฒฐ๊ณผ: /posts/123/opengraph-image URL๋กœ ์ ‘๊ทผํ•˜๋ฉด ํ•ด๋‹น ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ์ด ๋‹ด๊ธด ์ด๋ฏธ์ง€๊ฐ€ PNG๋กœ ๋ฐ˜ํ™˜๋ผ.

๐Ÿ“– ์šฉ์–ด: Satori โ€” Vercel์ด ๋งŒ๋“  JSX โ†’ SVG ๋ณ€ํ™˜ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์•ผ. ๋ธŒ๋ผ์šฐ์ € ์—†์ด ์„œ๋ฒ„์—์„œ HTML/CSS ๋ ˆ์ด์•„์›ƒ์„ ์ด๋ฏธ์ง€๋กœ ๋ Œ๋”๋งํ•ด. ํ•œ์ž๋กœ '๊นจ๋‹ฌ์Œ'์ด๋ผ๋Š” ๋œป.

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
opengraph-image.tsx๋Š” "์ž๋™ ํฌ์Šคํ„ฐ ์ œ์ž‘๊ธฐ"์•ผ. ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ๋งŒ ๋„ฃ์œผ๋ฉด ๋งํฌ ๊ณต์œ ์šฉ ์ธ๋„ค์ผ์ด ์ž๋™์œผ๋กœ ๋งŒ๋“ค์–ด์ ธ.


โšก ๊ณ ๊ธ‰ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ: robots, sitemap, canonical ๐Ÿ”ด

๐ŸŽฏ ์ด ์„น์…˜์„ ์ฝ๊ณ  ๋‚˜๋ฉด:

  • ๊ฒ€์ƒ‰์—”์ง„ ํฌ๋กค๋ง ๋ฒ”์œ„๋ฅผ robots.txt๋กœ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋‹ค
  • sitemap.ts๋กœ ๊ตฌ๊ธ€์— ๋ชจ๋“  ํŽ˜์ด์ง€ URL์„ ์ž๋™ ์ œ์ถœํ•  ์ˆ˜ ์žˆ๋‹ค

๐Ÿค– robots.txt โ€” ๋ด‡ ์ถœ์ž… ํ†ต์ œ

// app/robots.ts โ€” ์ž๋™์œผ๋กœ /robots.txt ์ƒ์„ฑ
import type { MetadataRoute } from 'next'
 
export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: '*',        // ๋ชจ๋“  ๋ด‡
        allow: '/',            // ์ „์ฒด ํ—ˆ์šฉ
        disallow: [
          '/api/',             // API ๊ฒฝ๋กœ๋Š” ๋ด‡ ์ฐจ๋‹จ
          '/admin/',           // ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€ ์ฐจ๋‹จ
          '/my/',              // ๋‚ด ์ •๋ณด ํŽ˜์ด์ง€ ์ฐจ๋‹จ
        ],
      },
    ],
    sitemap: 'https://youngsu.community/sitemap.xml',
  }
}

๐Ÿ—บ๏ธ sitemap.xml โ€” ๊ตฌ๊ธ€์— ํŽ˜์ด์ง€ ๋ชฉ๋ก ์ œ์ถœ

// app/sitemap.ts โ€” ์ž๋™์œผ๋กœ /sitemap.xml ์ƒ์„ฑ
import type { MetadataRoute } from 'next'
 
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  // ์ •์  ํŽ˜์ด์ง€
  const staticPages: MetadataRoute.Sitemap = [
    { url: 'https://youngsu.community', lastModified: new Date(), changeFrequency: 'daily', priority: 1 },
    { url: 'https://youngsu.community/posts', lastModified: new Date(), changeFrequency: 'hourly', priority: 0.9 },
  ]
 
  // ๋™์  ๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€ โ€” DB์—์„œ ์ „์ฒด ID ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
  const posts = await fetch('https://api.youngsu.community/posts/ids').then((r) => r.json())
  const postPages: MetadataRoute.Sitemap = posts.map((id: string) => ({
    url: `https://youngsu.community/posts/${id}`,
    lastModified: new Date(),
    changeFrequency: 'weekly' as const,
    priority: 0.8,
  }))
 
  return [...staticPages, ...postPages]
}

๐Ÿ”— canonical โ€” ์ค‘๋ณต URL ์ •๋ฆฌ

๊ฐ™์€ ๋‚ด์šฉ์ด ์—ฌ๋Ÿฌ URL์— ์กด์žฌํ•  ๋•Œ(์˜ˆ: ํŽ˜์ด์ง€๋„ค์ด์…˜, ํ•„ํ„ฐ) ๊ตฌ๊ธ€์ด ์–ด๋–ค ๊ฑธ ์›๋ณธ์œผ๋กœ ๋ด์•ผ ํ•˜๋Š”์ง€ ์•Œ๋ ค์ค˜.

// app/posts/page.tsx
export const metadata: Metadata = {
  alternates: {
    canonical: 'https://youngsu.community/posts',   // ์›๋ณธ URL ๋ช…์‹œ
  },
}

๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด. ๋Œ€๋ถ€๋ถ„ ์—ฌ๊ธฐ ์žˆ์–ด.


โŒ generateMetadata ์—์„œ params๋ฅผ await ์•ˆ ํ•ด์„œ ์—๋Ÿฌ

์–ธ์ œ ๋‚˜์˜ค๋Š”๊ฐ€?

Error: params should be awaited before using its properties.

์›์ธ: Next.js 15๋ถ€ํ„ฐ params๊ฐ€ Promise๋กœ ๋ณ€๊ฒฝ๋จ. await ์—†์ด ๋ฐ”๋กœ ์“ฐ๋ฉด ์—๋Ÿฌ.

ํ•ด๊ฒฐ์ฑ…:

// โŒ ์ด์ „ ๋ฐฉ์‹ (Next.js 14 ์ดํ•˜)
export async function generateMetadata({ params }: { params: { id: string } }) {
  const id = params.id   // ์—๋Ÿฌ!
}
 
// โœ… Next.js 15+ ๋ฐฉ์‹
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params  // await ํ•„์ˆ˜
}

โŒ OG ์ด๋ฏธ์ง€์—์„œ ํฐํŠธ๊ฐ€ ๊นจ์ ธ์„œ ํ•œ๊ธ€์ด ๋„ค๋ชจ๋กœ ํ‘œ์‹œ๋จ

์›์ธ: Satori ์—”์ง„์€ ๊ธฐ๋ณธ ํฐํŠธ๊ฐ€ ์˜๋ฌธ๋งŒ ์ง€์›ํ•ด. ํ•œ๊ธ€์€ ๋ณ„๋„ ํฐํŠธ ํŒŒ์ผ์„ ์ œ๊ณตํ•ด์•ผ ํ•ด.

ํ•ด๊ฒฐ์ฑ…:

// app/posts/[id]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
 
// ํ•œ๊ธ€ ํฐํŠธ ํŒŒ์ผ ๋กœ๋“œ (public ํด๋”์— ๋ฏธ๋ฆฌ ๋„ฃ์–ด๋‘ฌ์•ผ ํ•จ)
const fontData = await fetch(
  new URL('../../../../public/fonts/Noto_Sans_KR.ttf', import.meta.url)
).then((res) => res.arrayBuffer())
 
return new ImageResponse(
  <div>...</div>,
  {
    ...size,
    fonts: [
      { name: 'Noto Sans KR', data: fontData, style: 'normal' },
    ],
  }
)

โŒ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ž‘๋™ ์•ˆ ํ•จ

์›์ธ: metadata export์™€ generateMetadata๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ๋™์ž‘ํ•ด. 'use client' ํŒŒ์ผ์—์„œ export ํ•˜๋ฉด ๋ฌด์‹œ๋จ.

ํ•ด๊ฒฐ์ฑ…: ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ํ•ญ์ƒ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ(layout.tsx, page.tsx)์—์„œ exportํ•ด์•ผ ํ•ด. ํด๋ผ์ด์–ธํŠธ ๋กœ์ง์ด ํ•„์š”ํ•˜๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ„๋ฆฌํ•ด.


๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

์˜ค๋Š˜ ๋ฐฐ์šด ํ•ต์‹ฌ์„ ํ•œ๋ˆˆ์— ์ •๋ฆฌํ•ด๋ณผ๊นŒ? SEO ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ๋•Œ ์ด๊ฒƒ๋งŒ ๋ด๋„ ๋ผ.

๐Ÿ“‹ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์„ค์ • ํŒจํ„ด

์ƒํ™ฉ๋ฐฉ๋ฒ•ํŒŒ์ผ ์œ„์น˜
์‚ฌ์ดํŠธ ์ „์ฒด ๊ธฐ๋ณธ๊ฐ’export const metadataapp/layout.tsx
ํŠน์ • ์„น์…˜ ๊ธฐ๋ณธ๊ฐ’export const metadataapp/[section]/layout.tsx
๋™์  ํŽ˜์ด์ง€ (DB ์กฐํšŒ)export async function generateMetadataapp/[section]/[id]/page.tsx
๋งํฌ ๊ณต์œ  ์ธ๋„ค์ผopengraph-image.tsxapp/[section]/[id]/opengraph-image.tsx
๋ด‡ ์ ‘๊ทผ ์ œ์–ดexport default function robots()app/robots.ts
๊ตฌ๊ธ€ ํŽ˜์ด์ง€ ๋ชฉ๋กexport default async function sitemap()app/sitemap.ts

โš ๏ธ ์ ˆ๋Œ€ ํ•˜์ง€ ๋ง ๊ฒƒ

์ƒํ™ฉโŒ ๋‚˜์œ ์˜ˆโœ… ์ข‹์€ ์˜ˆ
๋™์  ํƒ€์ดํ‹€useEffect(() => { document.title = ... })generateMetadata ํ•จ์ˆ˜
ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ'use client' ํŒŒ์ผ์— metadata export์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ์— export
ํ•œ๊ธ€ OG ์ด๋ฏธ์ง€ ํฐํŠธ๊ธฐ๋ณธ ํฐํŠธ ๊ทธ๋Œ€๋กœ ์‚ฌ์šฉํ•œ๊ธ€ TTF ํŒŒ์ผ ๋ช…์‹œ์ ์œผ๋กœ ์ œ๊ณต

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

Q1. ๋‹ค์Œ ์ค‘ App Router์˜ Metadata API์— ๋Œ€ํ•œ ์„ค๋ช…์œผ๋กœ ํ‹€๋ฆฐ ๊ฒƒ์€?

  • A) layout.tsx์—์„œ metadata๋ฅผ exportํ•˜๋ฉด ํ•˜์œ„ ํŽ˜์ด์ง€์— ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ ์šฉ๋œ๋‹ค
  • B) generateMetadata๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค
  • C) opengraph-image.tsx๋Š” JSX๋ฅผ PNG ์ด๋ฏธ์ง€๋กœ ๋ณ€ํ™˜ํ•ด์ค€๋‹ค
  • D) ๋™์  ๋ผ์šฐํŠธ์—์„œ DB ๋ฐ์ดํ„ฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ <title>์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋‹ค

โœ… ์ •๋‹ต: B โ€” generateMetadata์™€ metadata export๋Š” ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์ „์šฉ์ด์•ผ.

์˜ค๋‹ต ํ•ด์„ค:

  • A โ€” ๋งž์•„. layout.tsx์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ํ•˜์œ„๋กœ ์ƒ์†(๋ณ‘ํ•ฉ)๋จ
  • C โ€” ๋งž์•„. opengraph-image.tsx๋Š” Satori ์—”์ง„์œผ๋กœ JSX โ†’ PNG ๋ณ€ํ™˜
  • D โ€” ๋งž์•„. generateMetadata๋Š” async ํ•จ์ˆ˜๋ผ DB ์กฐํšŒ ๊ฐ€๋Šฅ

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: metadata๋Š” ์„œ๋ฒ„์—์„œ๋งŒ! ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋Š” ์†๋Œ€์ง€ ๋งˆ.


Q2. ์•„๋ž˜ ๋นˆ์นธ์„ ์ฑ„์›Œ๋ณด์ž.

๋ฃจํŠธ ๋ ˆ์ด์•„์›ƒ์—์„œ title.template์„ '%s | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ'๋กœ ์„ค์ •ํ–ˆ์„ ๋•Œ,
๊ฒŒ์‹œ๊ธ€ ํŽ˜์ด์ง€์—์„œ title: 'ํด๋กœ์ € ์Šคํ„ฐ๋””'๋ฅผ ์„ค์ •ํ•˜๋ฉด <title> ํƒœ๊ทธ์˜ ์ตœ์ข… ๊ฐ’์€?

โœ… ์ •๋‹ต: ํด๋กœ์ € ์Šคํ„ฐ๋”” | ์˜์ˆ˜ ์ปค๋ฎค๋‹ˆํ‹ฐ

ํ•ด์„ค: %s ์ž๋ฆฌ์— ํ•˜์œ„ ํŽ˜์ด์ง€์˜ title ๊ฐ’์ด ๋“ค์–ด๊ฐ€๋Š” ํ…œํ”Œ๋ฆฟ ํŒจํ„ด์ด์•ผ.

๐Ÿ“Œ ํ•ต์‹ฌ ๊ธฐ์–ต๋ฒ•: %s๋Š” ์ž๋ฆฌํ‘œ์‹œ์ž(placeholder). "์—ฌ๊ธฐ์— ํŽ˜์ด์ง€ ์ด๋ฆ„ ๋“ค์–ด์™€" ๋ผ๋Š” ๋œป.


Q3. ์นœ๊ตฌ์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด?

OG ์ด๋ฏธ์ง€๊ฐ€ ์™œ SEO์—์„œ ์ค‘์š”ํ•œ์ง€, ๊ทธ๋ฆฌ๊ณ  Next.js์—์„œ ์–ด๋–ป๊ฒŒ ์ž๋™ ์ƒ์„ฑํ•˜๋Š”์ง€ ๋น„์œ ๋ฅผ ํ•˜๋‚˜ ์จ์„œ ์„ค๋ช…ํ•ด๋ด.

์˜ˆ์‹œ ๋‹ต๋ณ€:

"OG ์ด๋ฏธ์ง€๋Š” ๋งํฌ ๊ณต์œ ์šฉ ์ฑ… ํ‘œ์ง€์•ผ. ์ œ๋ชฉ๋งŒ ์žˆ๋Š” ์ฑ…์ด๋ž‘ ๋ฉ‹์ง„ ํ‘œ์ง€๊ฐ€ ์žˆ๋Š” ์ฑ… ์ค‘์— ์–ด๋–ค ๊ฑธ ๋” ํด๋ฆญํ•˜๊ฒ ์–ด? Next.js์˜ opengraph-image.tsx๋Š” ์ฑ… ํ‘œ์ง€ ์ธ์‡„๊ธฐ์•ผ. ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ๋งŒ ๋„ฃ์œผ๋ฉด ํ‘œ์ง€๋ฅผ ์ž๋™์œผ๋กœ ๋ฝ‘์•„์ค˜."


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ ์šฐ๋ฆฌ ์„œ๋น„์Šค์˜ '๊ฐ„ํŒ'์„ ๋‹ค๋Š” ๋‚ ์ด์—ˆ์–ด. ๊ตฌ๊ธ€์— ์šฐ๋ฆฌ ์„œ๋น„์Šค ์ด๋ฆ„์„ ๊ฒ€์ƒ‰ํ•ด๋„ ์•ˆ ๋‚˜์™€์„œ ์†์ƒํ–ˆ๋Š”๋ฐ, ์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ์•Œ๋ ค์ฃผ์‹  'Metadata API' ๋กœ ์‚ฌ์ดํŠธ์˜ ์ •์ฒด์„ฑ์„ ๋ช…ํ™•ํ•˜๊ฒŒ ์•Œ๋ ค์คฌ๊ฑฐ๋“ !

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋Š” ์„œ๋น„์Šค์˜ ๋ช…ํ•จ์ด๋‹ค. ๋ด‡๊ณผ ์‚ฌ๋žŒ์ด ์šฐ๋ฆฌ ์„œ๋น„์Šค๋ฅผ ์ž˜ ์ฐพ์„ ์ˆ˜ ์žˆ๊ฒŒ ์นœ์ ˆํ•œ ๊ฐ„ํŒ(SEO)์„ ๋‹ฌ์•„์ฃผ์ž."

ํŠนํžˆ ์นด์นด์˜คํ†ก์œผ๋กœ ๋งํฌ๋ฅผ ๋ณด๋ƒˆ์„ ๋•Œ ๊ฒŒ์‹œ๊ธ€ ์ œ๋ชฉ์ด ๋‹ด๊ธด ์˜ˆ์œ ์ธ๋„ค์ผ(Dynamic OG Image)์ด ๋™‡! ๋œจ๋Š” ๊ฑธ ๋ณด๊ณ  ์ •๋ง ๊ฐ๋™ํ–ˆ์–ด. ๋‹จ์ˆœํžˆ ์ฝ”๋“œ๋ฅผ ์งœ๋Š” ๊ฑธ ๋„˜์–ด, ์„ธ์ƒ์— ์šฐ๋ฆฌ ์„œ๋น„์Šค๋ฅผ ์–ด๋–ป๊ฒŒ ๋ณด์—ฌ์ค„์ง€ ์„ค๊ณ„ํ•˜๋Š” ๊ฒŒ ์–ผ๋งˆ๋‚˜ ์ค‘์š”ํ•œ์ง€ ๊นจ๋‹ฌ์•˜์–ด. ์˜ค๋Š˜ ์ •๋ง ๊ณ ์ƒํ•œ ๋‚˜ ์ž์‹ ์—๊ฒŒ ๋ง›์žˆ๋Š” ์•„์ด์Šคํฌ๋ฆผ ํ•˜๋‚˜ ์„ ๋ฌผํ•ด์•ผ์ง€. ๋‚ด์ผ์€ ๋” '๊ฒ€์ƒ‰ ์นœํ™”์ ์ธ' ๋ฉ‹์ง„ ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด๋ณผ ๊ฑฐ์•ผ! ๐Ÿฃ


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ