문서
App Router 사용하기
Metadata

메타데이터

Next.js에는 애플리케이션 메타데이터(예: HTML head 요소 내의 metalink 태그)를 정의하는 데 사용할 수 있는 메타데이터 API가 있어 SEO와 웹 공유성을 향상시킬 수 있습니다.

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

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

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

정적 메타데이터

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

layout.tsx | page.tsx
import type { Metadata } from "next";
 
export const metadata: Metadata = {
  title: "...",
  description: "...",
};
 
export default function Page() {}
layout.js | page.js
export const metadata = {
  title: "...",
  description: "...",
};
 
export default function Page() {}

사용 가능한 모든 옵션은 API 참조를 참조하세요.

동적 메타데이터

동적 값이 필요한 메타데이터를 fetch하기 위해 generateMetadata 함수를 사용할 수 있습니다.

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) {}
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를 통한 정적 및 동적 메타데이터는 서버 컴포넌트에서만 지원됩니다.
  • fetch 요청은 generateMetadata, generateStaticParams, 레이아웃, 페이지 및 서버 컴포넌트에서 동일한 데이터에 대해 자동으로 메모이제이션됩니다. fetch를 사용할 수 없는 경우 React cache를 사용할 수 있습니다.
  • Next.js는 클라이언트로 UI를 스트리밍하기 전에 generateMetadata 내부의 데이터 가져오기가 완료될 때까지 기다립니다. 이는 스트리밍된 응답의 첫 부분에 <head> 태그가 포함되도록 보장합니다.

파일 기반 메타데이터

메타데이터에 사용할 수 있는 특별한 파일은 다음과 같습니다:

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

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

동작

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

기본 필드

라우트에 메타데이터가 정의되어 있지 않더라도 항상 추가되는 두 개의 기본 meta 태그가 있습니다:

  • meta charset 태그는 웹사이트의 문자 인코딩을 설정합니다.
  • meta viewport 태그는 다양한 장치에 맞게 조정하기 위해 웹사이트의 뷰포트 너비와 스케일을 설정합니다.
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

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

순서

메타데이터는 루트 세그먼트에서 시작하여 최종 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 is a...",
  },
};
app/blog/page.js
export const metadata = {
  title: "Blog",
  openGraph: {
    title: "Blog",
  },
};
 
// 출력:
// <title>Blog</title>
// <meta property="og:title" content="Blog" />

위의 예에서:

  • app/layout.jstitleapp/blog/page.jstitle대체됩니다.
  • app/layout.js의 모든 openGraph 필드는 app/blog/page.js에서 openGraph 메타데이터를 설정하기 때문에 app/blog/page.js에서 대체됩니다. 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: "Home",
  },
};
app/about/page.js
import { openGraphImage } from "../shared-metadata";
 
export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: "About",
  },
};

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

필드 상속

app/layout.js
export const metadata = {
  title: "Acme",
  openGraph: {
    title: "Acme",
    description: "Acme is a...",
  },
};
app/about/page.js
export const metadata = {
  title: "About",
};
 
// 출력:
// <title>About</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme is a..." />

참고

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

동적 이미지 생성

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

사용하려면 next/og에서 ImageResponse를 가져올 수 있습니다:

app/about/route.js
import { ImageResponse } from "next/og";
 
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",
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 600,
    }
  );
}

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

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

알아두면 좋은 점:

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

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>
  );
}
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>
  );
}

Google의 Rich Results Test 또는 일반 Schema Markup Validator를 사용하여 구조화된 데이터를 검증하고 테스트할 수 있습니다.

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

import { Product, WithContext } from "schema-dts";
 
const jsonLd: WithContext<Product> = {
  "@context": "https://schema.org",
  "@type": "Product",
  name: "Next.js Sticker",
  image: "https://nextjs.org/imgs/sticker.png",
  description: "Dynamic at the speed of static.",
};