ReactNextCentral

메타데이터

Published on
모든 레이아웃이나 페이지에서 메타데이터를 정의하기 위해 메타데이터 API를 사용하세요.
Table of Contents

Next.js는 SEO와 웹 공유성을 향상시키기 위해 HTML head 요소 내부의 metalink 태그 같은 애플리케이션 메타데이터를 정의하는 데 사용할 수 있는 Metadata API를 제공합니다.

애플리케이션에 메타데이터를 추가하는 두 가지 방법이 있습니다.

  1. 설정 기반 메타데이터: layout.js 또는 page.js 파일에서 정적 metadata 객체나 동적 generateMetadata 함수를 내보냅니다.
  2. 파일 기반 메타데이터: 라우트 세그먼트에 정적 또는 동적으로 생성된 특별한 파일을 추가합니다.

이 두 옵션을 사용하면 Next.js는 페이지에 대한 관련 <head> 요소를 자동으로 생성합니다. 또한 ImageResponse 생성자를 사용하여 동적 OG 이미지도 생성할 수 있습니다.


정적 메타데이터

정적 메타데이터를 정의하려면 layout.js 또는 정적 page.js 파일에서 Metadata 객체를 내보냅니다.

layout.tsx
import type { Metadata } from 'next'

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

export default function Page() {}
JavaScript
layout.js
export const metadata = {
  title: '...',
  description: '...',
}

export default function Page() {}

사용 가능한 모든 옵션은 API 참조에서 확인할 수 있습니다.


동적 메타데이터

generateMetadata 함수를 사용하여 동적 값을 요구하는 메타데이터를 fetch 할 수 있습니다.

app/products/[id]/page.tsx
import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata(
  { params, searchParams }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  // 라우트 매개변수 읽기
  const id = params.id

  // 데이터 가져오기
  const product = await fetch(
    `https://.../${id}`).then((res) => res.json()
  )

  // 부모 메타데이터에 접근하고 확장 (대체하기보다는)하는 옵션
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

export default function Page({ params, searchParams }: Props) {}
JavaScript
app/products/[id]/page.js
export async function generateMetadata({ params, searchParams }, parent) {
  // 라우트 매개변수 읽기
  const id = params.id

  // 데이터 가져오기
  const product = await fetch(
    `https://.../${id}`).then((res) => res.json()
  )

  // 부모 메타데이터에 접근하고 확장 (대체하기보다는)하는 옵션
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

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

사용 가능한 모든 매개변수에 대해 API 참조에서 확인하세요.

  • generateMetadata를 통한 정적 및 동적 메타데이터는 Server Components에서만 지원됩니다.
  • fetch 요청은 generateMetadata, generateStaticParams, Layouts, Pages 및 Server Components의 동일한 데이터에 대해 자동으로 메모화됩니다. fetch를 사용할 수 없는 경우 React cache를 사용할 수 있습니다.
  • Next.js는 클라이언트에 UI를 스트리밍하기 전에 generateMetadata 내의 데이터 가져오기가 완료될 때까지 기다립니다. 이렇게 하면 스트리밍된 응답의 첫 번째 부분에 <head> 태그가 포함됩니다.

파일 기반 메타데이터

메타데이터를 위해 사용할 수 있는 특별한 파일들은 다음과 같습니다.

이들 파일들을 정적 메타데이터로 사용하거나 코드로 이 파일들을 프로그래밍적으로 생성할 수 있습니다.

구현과 예제는 메타데이터 파일 API 참조 및 동적 이미지 생성에서 확인하세요.


동작

파일 기반 메타데이터는 더 높은 우선순위를 가지며, 설정 기반 메타데이터를 덮어씁니다.

기본 필드

메타데이터를 정의하지 않는 경로에도 항상 추가되는 두 개의 기본 meta 태그가 있습니다.

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

알아두면 좋은 점: 기본 viewport 메타 태그를 덮어쓸 수 있습니다.

순서

메타데이터는 최종 page.js 세그먼트에 가장 가까운 세그먼트로부터 루트 세그먼트까지 순서대로 평가됩니다. 예를 들면 아래와 같습니다.

  1. app/layout.tsx (루트 레이아웃)
  2. app/blog/layout.tsx (중첩된 블로그 레이아웃)
  3. app/blog/[slug]/page.tsx (블로그 페이지)

병합

평가 순서를 따라 같은 경로의 여러 세그먼트에서 내보낸 메타데이터 객체는 경로의 최종 메타데이터 출력물을 형성하기 위해 얕게 병합됩니다. 중복 키는 순서에 따라 대체됩니다.

이것은 openGraphrobots와 같이 중첩된 필드를 가진 메타데이터가 이전 세그먼트에서 정의되면 마지막 세그먼트에서 덮어쓰게 됨을 의미합니다.

필드 덮어쓰기 예제 코드는 다음과 같습니다.

app/layout.js"
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme는 ...입니다.',
  },
}
JavaScript
app/blog/page.js"
export const metadata = {
  title: '블로그',
  openGraph: {
    title: '블로그',
  },
}

// 출력:
// <title>블로그</title>
// <meta property="og:title" content="블로그" />

위 예제는 아래와 같이 동작됩니다.

  • app/layout.jstitleapp/blog/page.jstitle대체됩니다.
  • app/layout.js의 모든 openGraph 필드는 app/blog/page.js에서 openGraph 메타데이터를 설정하기 때문에 대체됩니다. openGraph.description이 없는 것에 주목하세요.

세그먼트간에 중첩된 필드를 공유하면서 다른 필드를 덮어쓰고 싶다면 별도의 변수로 분리할 수 있습니다.

app/shared-metadata.js
export const openGraphImage = { images: ['http://...'] }
app/page.js
import { openGraphImage } from './shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: '홈',
  },
}
JavaScript
app/about/page.js
import { openGraphImage } from '../shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: '소개',
  },
}

위 예제에서 OG 이미지는 app/layout.jsapp/about/page.js 사이에서 공유되며 타이틀은 다릅니다.

필드 상속

app/layout.js
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme는 ...입니다.',
  },
}
app/about/page.js
export const metadata = {
  title: '소개',
}

// 출력:
// <title>소개</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme는 ...입니다." />

주의사항은 다음과 같습니다.

  • app/layout.jstitleapp/about/page.jstitle대체됩니다.
  • app/about/page.jsopenGraph 메타데이터를 설정하지 않기 때문에 app/layout.js의 모든 openGraph 필드가 상속됩니다.

동적 이미지 생성

ImageResponse 생성자를 사용하면 JSX와 CSS를 이용하여 동적 이미지를 생성할 수 있습니다. 이는 Open Graph 이미지, Twitter 카드 등의 소셜 미디어 이미지를 생성하기에 유용합니다.

ImageResponseEdge Runtime을 사용하며, Next.js는 자동으로 엣지에서 캐싱된 이미지에 올바른 헤더를 추가하여 성능을 향상시키고 다시 계산을 줄입니다.

이를 사용하려면 next/server에서 ImageResponse를 가져올 수 있습니다.

app/about/route.js
import { ImageResponse } from 'next/server'

export const runtime = 'edge'

export async function GET() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          textAlign: 'center',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        안녕, 세상아!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  )
}

ImageResponseRoute Handlers 및 파일 기반 메타데이터를 포함한 다른 Next.js API와 잘 통합됩니다. 예를 들어, opengraph-image.tsx 파일에서 ImageResponse를 사용하여 빌드 시간 또는 요청 시간에 동적으로 Open Graph 이미지를 생성할 수 있습니다.

ImageResponse는 flexbox와 절대 위치 지정, 사용자 정의 폰트, 텍스트 줄 바꿈, 중앙 정렬 및 중첩된 이미지를 포함한 일반적인 CSS 속성을 지원합니다. 지원되는 CSS 속성의 전체 목록을 확인하십시오.

  • 예제는 Vercel OG Playground에서 확인할 수 있습니다.
  • ImageResponse는 HTML과 CSS를 PNG로 변환하기 위해 @vercel/og, Satori, 및 Resvg를 사용합니다.
  • 오직 Edge Runtime만 지원됩니다. 기본 Node.js 런타임은 작동하지 않습니다.
  • flexbox와 CSS 속성의 부분 집합만 지원됩니다. 고급 레이아웃(예: display: grid)은 작동하지 않습니다.
  • 번들 최대 크기는 500KB입니다. 번들 크기에는 JSX, CSS, 폰트, 이미지 및 기타 모든 자산이 포함됩니다. 제한을 초과하는 경우 자산의 크기를 줄이거나 런타임에 가져올 수 있습니다.
  • ttf, otf, 및 woff 폰트 형식만 지원됩니다. 폰트 파싱 속도를 최대화하기 위해 ttf 또는 otfwoff보다 선호됩니다.

JSON-LD

JSON-LD는 검색 엔진이 귀하의 콘텐츠를 이해하는 데 사용할 수 있는 구조화된 데이터 형식입니다. 예를 들면, 이를 사용하여 사람, 이벤트, 조직, 영화, 책, 레시피 등 많은 종류의 엔터티를 설명할 수 있습니다.

현재 JSON-LD에 대한 추천 방식은 layout.js 또는 page.js 컴포넌트 내에서 구조화된 데이터를 <script> 태그로 렌더링하는 것입니다. 아래는 예제 코드입니다.

app/products/[id]/page.tsx
export default async function Page({ params }) {
  const product = await getProduct(params.id)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description,
  }

  return (
    <section>
      {/* 페이지에 JSON-LD 추가 */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* ... */}
    </section>
  )
}
JavaScript
app/products/[id]/page.js
export default async function Page({ params }) {
  const product = await getProduct(params.id)

  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description,
  }

  return (
    <section>
      {/* 페이지에 JSON-LD 추가 */}
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* ... */}
    </section>
  )
}

구조화된 데이터를 Rich Results Test로 Google에 대하여 검증하고 테스트하거나 일반적인 Schema Markup Validator를 사용할 수 있습니다.

타입스크립트를 사용하여 schema-dts와 같은 커뮤니티 패키지로 JSON-LD를 타입화 할 수 있습니다.

import { Product, WithContext } from 'schema-dts'

const jsonLd: WithContext<Product> = {
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Next.js 스티커',
  image: 'https://nextjs.org/imgs/sticker.png',
  description: '정적의 속도에서 동적으로.',
}