Metadata and OG images

김동현·2026년 3월 4일

next.js 공식문서 번역

목록 보기
14/79

Next.js의 메타데이터(Metadata) API를 사용하면 더 나은 SEO(검색 엔진 최적화)와 웹 공유성을 위해 애플리케이션의 메타데이터를 손쉽게 정의할 수 있어요. 메타데이터 API는 크게 다음과 같이 나뉩니다.

  1. 정적 metadata 객체 (The static metadata object)
  2. 동적 generateMetadata 함수 (The dynamic generateMetadata function)
  3. 정적이거나 동적으로 생성되는 파비콘(favicons)OG 이미지(OG images)를 추가할 때 사용할 수 있는 특별한 파일 규칙들(file conventions)

💡 강사님의 실무 팁!
"메타데이터가 왜 그렇게 중요할까요? 카카오톡이나 슬랙에 링크를 복사해서 붙여넣었을 때 뜨는 예쁜 썸네일과 제목, 설명글 보신 적 있죠? 그게 바로 이 메타데이터, 특히 OG(Open Graph) 태그 덕분이에요. 마케팅이나 클릭률에 엄청난 영향을 미치기 때문에 실무 프론트엔드 개발자라면 꼭 마스터해야 하는 기능입니다."

위에서 말씀드린 방법들을 사용하면 Next.js가 페이지에 알맞은 <head> 태그들을 알아서 척척 생성해 줍니다. 브라우저의 개발자 도구를 열어서 어떻게 들어갔는지 직접 확인해 볼 수도 있죠.

주의할 점: metadata 객체와 generateMetadata 함수 내보내기(export)는 오직 서버 컴포넌트(Server Components)에서만 지원된답니다. 클라이언트 컴포넌트("use client")에서는 사용할 수 없어요!

기본 필드 (Default fields)

라우트(route)에서 메타데이터를 따로 정의하지 않더라도, Next.js가 언제나 기본으로 추가해 주는 두 가지 meta 태그가 있습니다.

  • meta charset 태그: 웹사이트의 문자 인코딩(character encoding)을 설정합니다. 글자가 깨지지 않게 해주는 필수 태그죠.
  • meta viewport 태그: 다양한 기기(스마트폰, 태블릿 등)에 맞춰 화면이 잘 보이도록 웹사이트의 뷰포트(viewport) 너비와 스케일을 조절해 줍니다. 모바일 반응형 웹을 위한 필수 요소예요.
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

이 두 가지 외에 다른 메타데이터 필드들은 앞서 말씀드린 Metadata 객체(정적 메타데이터)나 generateMetadata 함수(생성된 동적 메타데이터)를 이용해서 직접 정의할 수 있어요.

정적 메타데이터 (Static metadata)

변하지 않는 고정된 메타데이터를 정의하고 싶다면, 정적인 layout.jspage.js 파일에서 Metadata 객체를 export 해주면 됩니다.
예를 들어, 블로그 페이지에 고정된 제목(title)과 설명(description)을 추가하고 싶다면 아래처럼 작성할 수 있어요.

// filename="app/blog/layout.tsx" switcher
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My Blog',
  description: '...',
}

export default function Layout() {}
// filename="app/blog/layout.js" switcher
export const metadata = {
  title: 'My Blog',
  description: '...',
}

export default function Layout() {}

📖 보충 설명: 위 코드는 블로그 레이아웃 전체에 공통으로 적용되는 메타데이터를 설정한 거예요. 이렇게 해두면 /blog 하위 경로에 있는 페이지들은 기본적으로 이 메타데이터를 상속받게 됩니다. 사용할 수 있는 더 많은 옵션이 궁금하시다면 generateMetadata 공식 문서를 참고해 보세요!

동적 생성 메타데이터 (Generated metadata)

만약 블로그 포스트처럼 데이터(예: 글 제목, 내용)에 따라 메타데이터가 동적으로 바뀌어야 한다면 어떨까요? 이때는 generateMetadata 함수를 사용해서 데이터를 fetch(가져오기) 한 뒤 메타데이터를 만들면 됩니다.

예를 들어, 특정 블로그 포스트의 제목과 설명을 서버에서 가져와 메타데이터로 설정하는 코드를 볼까요?

// filename="app/blog/[slug]/page.tsx" switcher
import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const slug = (await params).slug

  // post 정보 가져오기 (fetch)
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
    res.json()
  )

  return {
    title: post.title,
    description: post.description,
  }
}

export default function Page({ params, searchParams }: Props) {}
// filename="app/blog/[slug]/page.js" switcher
export async function generateMetadata({ params, searchParams }, parent) {
  const slug = (await params).slug

  // post 정보 가져오기 (fetch)
  const post = await fetch(`https://api.vercel.app/blog/${slug}`).then((res) =>
    res.json()
  )

  return {
    title: post.title,
    description: post.description,
  }
}

export default function Page({ params, searchParams }) {}

스트리밍 메타데이터 (Streaming metadata)

동적으로 렌더링되는 페이지의 경우, Next.js는 UI 렌더링을 막지 않으면서 generateMetadata 처리가 완료되는 즉시 HTML에 메타데이터를 삽입하는 방식으로 '스트리밍(Streaming)'을 처리합니다.

이게 무슨 뜻이냐고요? 사용자에게 시각적인 콘텐츠(UI)를 먼저 스트리밍해서 보여줌으로써 사용자가 느끼는 체감 성능(perceived performance)을 크게 향상시켜 준다는 이야기예요! 화면이 빨리 뜨면 사용자는 웹사이트가 빠르다고 느끼니까요.

하지만 주의하세요! 이 스트리밍 메타데이터 기능은 HTML <head> 태그 안에 메타데이터가 온전히 다 들어있기를 기대하는 봇(bots)이나 크롤러(crawlers)에게는 비활성화되어 있습니다. (예: Twitterbot, Slackbot, Bingbot 등)
Next.js는 서버로 들어오는 요청(request)의 User Agent 헤더를 확인해서 이런 봇들을 자동으로 감지해 냅니다.

💡 강사님의 꿀팁:
"왜 봇들에게는 스트리밍을 끌까요? 크롤러들은 성격이 급해서 페이지 렌더링이 다 끝날 때까지 기다려주지 않고 HTML 문서의 앞부분만 쓱 훑어보고 가는 경우가 많아요. 메타데이터가 나중에 스트리밍으로 늦게 오면, 크롤러는 '어? 이 페이지는 제목도 없고 설명도 없네?' 하고 그냥 넘어가 버릴 수 있거든요. 그래서 Next.js가 아주 똑똑하게 봇이 오면 스트리밍 없이 한 번에 완성된 HTML을 보내주는 거랍니다!"

이러한 스트리밍 메타데이터 기능을 완전히 커스터마이징하거나 비활성화하고 싶다면, Next.js 설정 파일(next.config.js)의 htmlLimitedBots 옵션을 사용하시면 됩니다.

참고로, 빌드 시점에 메타데이터가 이미 다 결정되어 있는 '정적 렌더링(Statically rendered) 페이지'들은 애초에 스트리밍 방식을 사용하지 않습니다.

스트리밍 메타데이터에 대해 더 자세히 알고 싶다면 스트리밍 메타데이터 문서를 확인해 보세요.

데이터 요청 메모이제이션 (Memoizing data requests)

개발을 하다 보면, 메타데이터를 만들기 위해서도 데이터를 가져와야 하고, 실제 페이지 화면을 그리기 위해서도 동일한 데이터를 또 가져와야 하는 경우가 생깁니다. 아까 위에서 본 블로그 포스트 예시가 딱 그렇죠?

이럴 때 똑같은 네트워크 요청이나 DB 조회를 두 번씩 하면 비효율적이잖아요? 불필요한 중복 요청을 막기 위해 우리는 React가 제공하는 cache 함수를 사용할 수 있습니다. cache 함수는 함수의 반환값을 기억(메모이제이션)해 두었다가, 데이터를 딱 한 번만 가져오게 해줍니다.

예를 들어, 메타데이터와 페이지 컴포넌트 양쪽 모두에서 블로그 포스트 정보를 가져와야 할 때의 코드를 볼까요?

// filename="app/lib/data.ts" highlight={5} switcher
import { cache } from 'react'
import { db } from '@/app/lib/db'

// getPost 함수는 두 번 호출되지만, 실제 실행(DB 조회)은 한 번만 일어납니다!
export const getPost = cache(async (slug: string) => {
  const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
  return res
})
// filename="app/lib/data.js" highlight={5} switcher
import { cache } from 'react'
import { db } from '@/app/lib/db'

// getPost 함수는 두 번 호출되지만, 실제 실행(DB 조회)은 한 번만 일어납니다!
export const getPost = cache(async (slug) => {
  const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
  return res
})
// filename="app/blog/[slug]/page.tsx" switcher
import { getPost } from '@/app/lib/data'

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug) // 첫 번째 호출 (여기서 데이터를 가져옴)
  return {
    title: post.title,
    description: post.description,
  }
}

export default async function Page({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug) // 두 번째 호출 (캐시된 데이터를 바로 사용!)
  return <div>{post.title}</div>
}
// filename="app/blog/[slug]/page.js" switcher
import { getPost } from '@/app/lib/data'

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug)
  return {
    title: post.title,
    description: post.description,
  }
}

export default async function Page({ params }) {
  const post = await getPost(params.slug)
  return <div>{post.title}</div>
}

파일 기반 메타데이터 (File-based metadata)

코드를 작성하지 않고도 특정 이름의 파일을 제자리에 두는 것만으로 메타데이터를 설정할 수 있는 특별한 파일 규칙들이 있습니다.

이 파일들을 준비해서 정적 메타데이터로 사용할 수도 있고, 또는 코드를 활용해 프로그래밍 방식으로 이 파일들을 동적 생성할 수도 있습니다.

파비콘 (Favicons)

파비콘은 브라우저의 즐겨찾기(북마크)나 검색 결과 탭에 표시되는 우리 사이트의 작은 대표 아이콘입니다. 애플리케이션에 파비콘을 추가하려면, 간단히 favicon.ico 파일을 만들어서 app 폴더의 루트(최상위) 경로에 넣어두면 됩니다.

Favicon Special File inside the App Folder with sibling layout and page files

코드를 사용해서 파비콘을 동적으로 생성할 수도 있어요. 자세한 내용은 파비콘 공식 문서를 참고해 주세요.

정적 Open Graph 이미지 (Static Open Graph images)

오픈 그래프(Open Graph, OG) 이미지는 소셜 미디어(페이스북, 트위터, 카카오톡 등)에서 우리 웹사이트의 링크가 공유될 때 표시되는 미리보기 대표 이미지입니다. 정적 OG 이미지를 추가하려면, app 폴더 루트에 opengraph-image.jpg 파일을 추가해 주세요.

OG image special file inside the App folder with sibling layout and page files

뿐만 아니라, 특정 라우트(경로)에만 전용 OG 이미지를 띄우고 싶다면 해당 폴더 구조 깊숙한 곳에 opengraph-image.jpg를 만들 수도 있어요. 예를 들어 /blog 라우트 전용 OG 이미지를 만들고 싶다면, blog 폴더 안에 opengraph-image.jpg 파일을 쏙 넣어주면 됩니다.

OG image special file inside the blog folder

이렇게 폴더 구조 내에 파일을 배치하면, 더 하위 폴더(구체적인 경로)에 있는 이미지가 그보다 상위에 있는 OG 이미지들을 덮어쓰고 우선적으로 적용됩니다.

jpg 뿐만 아니라 jpeg, png, gif 같은 다른 이미지 포맷도 지원됩니다. 더 자세한 건 오픈 그래프 이미지 문서를 확인해 보세요!

동적 생성 Open Graph 이미지 (Generated Open Graph images)

가장 흥미로운 기능 중 하나입니다! ImageResponse 생성자를 사용하면, JSX와 CSS를 가지고 프로그래밍 방식으로 '동적' 이미지를 그려낼 수 있어요. 특히 블로그 포스트나 상품 상세 페이지처럼 데이터에 따라 이미지가 매번 달라져야 하는 OG 이미지를 만들 때 아주 유용하죠.

예를 들어, 각 블로그 포스트마다 서로 다른 고유한 제목을 가진 OG 이미지를 생성하고 싶다고 해볼게요. blog 폴더 안에 opengraph-image.tsx 파일을 만들고, next/og 모듈에서 ImageResponse를 가져와서 이렇게 작성합니다.

// filename="app/blog/[slug]/opengraph-image.tsx" switcher
import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'

// 이미지 메타데이터 설정
export const size = {
  width: 1200,
  height: 630,
}

export const contentType = 'image/png'

// 이미지 생성기
export default async function Image({ params }: { params: { slug: string } }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      // ImageResponse 안에 들어갈 JSX 엘리먼트 (이대로 이미지가 구워집니다!)
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  )
}
// filename="app/blog/[slug]/opengraph-image.js" switcher
import { ImageResponse } from 'next/og'
import { getPost } from '@/app/lib/data'

// 이미지 메타데이터 설정
export const size = {
  width: 1200,
  height: 630,
}

export const contentType = 'image/png'

// 이미지 생성기
export default async function Image({ params }) {
  const post = await getPost(params.slug)

  return new ImageResponse(
    (
      // ImageResponse 안에 들어갈 JSX 엘리먼트 (이대로 이미지가 구워집니다!)
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    )
  )
}

💡 강사님의 실무 팁!
"이 기능 정말 혁신적이지 않나요? 예전에는 마케터나 디자이너가 게시글마다 일일이 썸네일 이미지를 만들어서 서버에 올려야 했어요. 하지만 이제 프론트엔드 개발자가 JSX와 CSS 스타일을 이용해 코드 템플릿만 짜두면, 글 제목이나 작성자 이름이 바뀔 때마다 서버에서 예쁜 이미지를 자동으로 생성(렌더링)해준답니다. 쇼핑몰이나 대규모 콘텐츠 사이트를 운영할 때 엄청난 시간 단축 효과를 가져다주죠!"

ImageResponse는 Flexbox(display: flex), 절대 위치(position: absolute), 커스텀 폰트, 텍스트 줄바꿈, 가운데 정렬, 그리고 중첩 이미지 등 개발자들에게 익숙한 일반적인 CSS 속성들을 대부분 지원합니다. 지원되는 전체 CSS 속성 목록은 이곳에서 확인하실 수 있어요.

알아두면 좋은 점 (Good to know):

  • Vercel OG Playground에 가시면 다양한 예제 코드들을 직접 테스트해 볼 수 있습니다. (꼭 한번 들어가서 만져보세요!)
  • 내부적으로 ImageResponse@vercel/og, satori, 그리고 resvg라는 훌륭한 라이브러리들을 엮어서 HTML과 CSS를 PNG 이미지로 변환해 냅니다.
  • 아주 중요한 제약사항이 하나 있어요! 오직 Flexbox와 일부 제한된 CSS 속성 집합만 지원됩니다. display: grid 같은 너무 복잡하고 고급화된 레이아웃은 동작하지 않으니 꼭 Flexbox 위주로 레이아웃을 짜셔야 해요.

API 레퍼런스 (API Reference)

이 페이지에서 언급된 메타데이터 API들에 대해 더 깊이 공부하고 싶으신 분들은 아래 링크들을 참고해 주세요!

  • generateMetadata
    • 더 나은 검색 엔진 최적화(SEO)와 공유를 위해 Next.js 앱에 메타데이터를 추가하는 자세한 방법을 알아봅니다.
  • generateViewport
    • generateViewport 함수에 대한 API 문서입니다.
  • ImageResponse
    • 동적 이미지를 만드는 ImageResponse 생성자 API 문서입니다.
  • 메타데이터 파일들 (Metadata Files)
    • 파일 기반 메타데이터 규칙들에 대한 전체적인 API 문서입니다.
  • favicon, icon, 그리고 apple-icon
    • 파비콘과 각종 아이콘 파일 규칙을 위한 API 문서입니다.
  • opengraph-image 그리고 twitter-image
    • OG 이미지와 트위터 이미지 파일 규칙에 대한 API 문서입니다.
  • robots.txt
    • 검색 엔진 로봇들의 접근을 제어하는 robots.txt 파일 API 문서입니다.
  • sitemap.xml
    • 사이트 구조를 검색 엔진에 알려주는 sitemap.xml 파일 API 문서입니다.
  • htmlLimitedBots
    • 메타데이터 스트리밍을 차단하고 완성된 HTML을 받아야 하는 특정 봇(User Agents) 리스트를 지정하는 방법입니다.

모든 문서의 의미론적(Semantic) 개요를 보시려면 /docs/sitemap.md를 참고하세요.

사용 가능한 모든 문서의 인덱스를 보시려면 /docs/llms.txt를 참고하세요.


자, 여기까지 Next.js의 메타데이터와 OG 이미지 문서 번역 및 부연 설명을 마쳤습니다. 번역된 내용이나 코드 중에 이해가 안 가는 부분이 있다면 언제든지 다시 질문해 주시겠어요?

profile
프론트에_가까운_풀스택_개발자

0개의 댓글