← 전체 글로 돌아가기

Next.js

Next.js에서 SEO 메타데이터 관리하기

Next.js의 동적 페이지에서 검색 엔진이 올바른 메타데이터를 인식하려면, 빌드와 배포 단계를 제대로 이해해야 합니다.

블로그 포스트를 배포했는데, Google 검색에서 제목이 안 보인다. 또는 SNS에 공유했을 때 미리보기가 없다. 메타데이터가 제대로 가지 않은 것이다.

메타데이터 확인하기

먼저 실제로 HTML에 메타데이터가 있는가?

# 배포 URL의 HTML 소스 확인
curl https://example.com/blog/my-post | grep -i meta

# 브라우저에서는 우클릭 → 페이지 소스 보기

HTML head에 다음이 있는가?

<meta property="og:title" content="제목" />
<meta property="og:description" content="설명" />
<meta property="og:image" content="https://example.com/image.jpg" />
<meta name="description" content="설명" />
<link rel="canonical" href="https://example.com/blog/my-post" />

Next.js Metadata API 사용

Next.js 13+ App Router에서는 generateMetadata를 사용한다.

// app/blog/[slug]/page.tsx
import { Metadata } from 'next';

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{
        url: post.imageUrl,
        width: 1200,
        height: 630,
      }],
      type: 'article',
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.imageUrl],
    },
  };
}

export default function Page({ params }) {
  return <BlogPost slug={params.slug} />;
}

빌드 vs 런타임

StaticGeneration (SSG)으로 빌드하면, 빌드 시점의 메타데이터가 구워진다.

// 나쁜 예: 런타임에 메타데이터를 가져온다
// 이미지 URL이 동적이면 빌드 시점에는 모를 수 있다
export async function generateMetadata({
  params,
}): Promise<Metadata> {
  // 빌드 시점에 여기 코드가 실행된다
  const post = await getPostFromAPI(params.slug); // API 호출
  return { title: post.title };
}

// 낫다: 빌드 시점에 데이터가 있어야 한다
export async function generateStaticParams() {
  const posts = await getAllPosts();
  return posts.map(post => ({ slug: post.slug }));
}

빌드 시점에 모든 데이터를 가져올 수 없으면 Dynamic Rendering을 써야 한다 (revalidate 사용).

동적 라우트 확인

[id] 같은 동적 라우트가 있으면, 빌드 시점에 어떤 페이지들을 생성할 것인지 알아야 한다.

// generateStaticParams를 구현해야 빌드할 때 페이지를 미리 생성한다
export async function generateStaticParams() {
  const posts = await db.post.findMany();
  return posts.map(post => ({
    slug: post.slug,
  }));
}

이 함수가 없으면 getPostBySlug는 요청 시점에 실행된다. 검색 엔진은 빠르게 스크래핑하므로 데이터를 못 가져올 수 있다.

Sitemap 확인

검색 엔진이 모든 페이지를 발견하려면 sitemap이 있어야 한다.

// app/sitemap.ts
import { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await getAllPosts();

  return posts.map(post => ({
    url: `https://example.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly',
    priority: 0.8,
  }));
}

OG 이미지 생성

OG 이미지 URL이 정적 이미지인가, 동적으로 생성되는가?

// 나쁜 예: 동적 이미지 URL
export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    openGraph: {
      images: [{
        url: `https://example.com/api/og?slug=${params.slug}`,
      }],
    },
  };
}

// 낫다: 사전에 생성된 이미지
export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    openGraph: {
      images: [{
        url: `https://example.com/og/${params.slug}.png`,
      }],
    },
  };
}

검색 엔진이 OG 이미지 URL을 방문할 때, API가 느리거나 실패하면 이미지를 수집하지 못한다.

Canonical URL 설정

SEO 중복을 피하려면 각 페이지의 canonical URL을 명시해야 한다.

export async function generateMetadata({ params }): Promise<Metadata> {
  return {
    alternates: {
      canonical: `https://example.com/blog/${params.slug}`,
    },
  };
}

배포 후 검증

배포한 후 실제로 검색 엔진이 메타데이터를 인식했는가?

# Google Rich Results Test
https://search.google.com/test/rich-results?url=https://example.com/blog/my-post

# Open Graph Preview
https://www.opengraphcheck.com

"Missing properties" 같은 경고가 있으면 뭐가 빠진 건지 확인한다.

배포 후 재크롤

Google Search Console에 등록하고, "URL 검사"로 크롤링을 요청한다.

# URL 검사
1. Google Search Console 열기
2. "URL 검사" 입력창
3. 포스트 URL 입력
4. "색인 생성 요청" 클릭

24시간 내에 색인 된다.

최종 확인: 개발 환경과 배포 비교

Dev 환경에서는 http://localhost:3000이지만, 배포 환경에서는 https://example.com이어야 한다.

const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || 'http://localhost:3000';

export async function generateMetadata(): Promise<Metadata> {
  return {
    openGraph: {
      images: [{
        url: `${baseUrl}/og/default.png`, // 환경에 맞게
      }],
    },
  };
}

marginBottom마지막으로, SEO는 빌드 시점부터 시작된다. 배포 후가 아니라 개발할 때부터 메타데이터를 제대로 설정해야 한다.