Next.js 페이지별 SEO 최적화 using next/head

CH.dev·2025년 8월 6일
post-thumbnail

📄 요약

전통적인 SPA(Single Page Application)는 클라이언트 사이드에서 document.title 등을 조작하여 SEO에 취약했음. Next.js는 서버에서 페이지를 렌더링하는 시점에 각 페이지에 최적화된 <head> 정보를 미리 HTML에 삽입함. next/head 컴포넌트는 이 과정을 직관적으로 만들어, 페이지별 동적 title, meta 태그 관리를 가능하게 하고 검색 엔진 최적화(SEO)를 극대화하는 핵심 도구임.

💡 핵심 원리 및 전략

1. next/head의 작동 원리

next/head는 React 컴포넌트 트리 어디에나 위치할 수 있음. Next.js는 렌더링 시점에 모든 next/head 컴포넌트의 자식들을 수집(collect)하여 최종 HTML의 <head> 태그 안에 주입함.

  • 태그 중복 제거(De-duplication): 동일한 name이나 property를 가진 meta 태그가 여러 번 선언되면, 컴포넌트 트리상 가장 마지막(가장 깊은 곳)에 선언된 태그만 렌더링됨. 이를 통해 공통 레이아웃에 설정된 기본값을 페이지별로 쉽게 덮어쓸 수 있음.
  • key Prop의 중요성: title, meta viewport처럼 페이지에 유일해야 하는 태그들은 Next.js가 자동으로 중복을 처리함. 하지만 og:image처럼 여러 개가 존재할 수 있는 태그를 유일하게 만들고 싶거나, 재정의 순서를 명확히 제어하고 싶을 때 key prop을 사용함. key가 동일한 태그는 하나만 렌더링됨.

2. SEO를 위한 필수 메타 태그

  • title: 검색 결과 최상단, 브라우저 탭에 노출. 사용자의 첫인상을 결정. (권장 길이: 50-60자)
  • meta name="description": 검색 결과의 요약문. 클릭률(CTR)에 직접적인 영향을 줌. (권장 길이: 150-160자)
  • meta name="viewport": 모바일 기기에서의 화면 렌더링 방식을 제어. width=device-width, initial-scale=1은 필수.
  • Open Graph (og:): 소셜 미디어 공유 시 표시될 콘텐츠(제목, 설명, 이미지, URL)를 정의.
  • link rel="canonical": 여러 URL이 동일한 콘텐츠를 가리킬 때, 검색 엔진에 어떤 URL이 '대표'인지 알려주어 중복 콘텐츠 페널티를 방지.

3. 컴포넌트 계층을 활용한 Head 관리 전략

효율적인 관리를 위해 컴포넌트 계층에 따라 Head를 설정하는 것이 좋음.

  1. _app.js (전역 기본값): 사이트 전체에 적용될 기본 title (또는 title 템플릿), viewport, 기본 og:site_name 등을 설정. 모든 페이지의 기반이 됨.
  2. Layout 컴포넌트 (페이지 유형별 기본값): 블로그, 상품 목록 등 특정 페이지 유형에 공통으로 적용될 head 정보를 설정.
  3. 개별 페이지 컴포넌트 (최종 값): 가장 구체적인 정보를 담음. _app.jsLayout의 기본값을 덮어씀. 예를 들어, 상품 상세 페이지의 상품명, 상품 이미지 등을 동적으로 설정.

🧠 예시 코드: 계층적 SEO 컴포넌트 설계

공통 SEO 설정을 위한 Seo 컴포넌트를 만들고, 이를 _app.js와 개별 페이지에서 계층적으로 활용하는 예시.

1. components/Seo.js (재사용 가능한 SEO 컴포넌트)

import Head from 'next/head';

// 기본값 설정
const defaultMeta = {
  title: 'My Awesome Store',
  description: '최고의 상품들을 만나보세요!',
  image: 'https://example.com/default-og-image.png',
  url: 'https://example.com',
};

function Seo({ title, description, image, url, children }) {
  const meta = { ...defaultMeta, ...{ title, description, image, url } };
  const pageTitle = title ? `${meta.title} | My Awesome Store` : defaultMeta.title;

  return (
    <Head>
      <title>{pageTitle}</title>
      <meta name="description" content={meta.description} />
      <link rel="canonical" href={meta.url} />

      {/* Open Graph */}
      <meta property="og:title" content={pageTitle} />
      <meta property="og:description" content={meta.description} />
      <meta property="og:image" content={meta.image} />
      <meta property="og:url" content={meta.url} />
      <meta property="og:type" content="website" />
      <meta property="og:site_name" content="My Awesome Store" />

      {/* Twitter Cards */}
      <meta name="twitter:card" content="summary_large_image" />
      <meta name="twitter:title" content={pageTitle} />
      <meta name="twitter:description" content={meta.description} />
      <meta name="twitter:image" content={meta.image} />
      
      {/* 추가적인 Head 태그를 위한 children */}
      {children}
    </Head>
  );
}

export default Seo;

2. pages/_app.js (전역 기본 SEO 적용)

import Seo from '../components/Seo';
import '../styles/globals.css';

function MyApp({ Component, pageProps }) {
  return (
    <>
      {/* 모든 페이지에 기본 SEO 값을 제공 */}
      <Seo />
      <Component {...pageProps} />
    </>
  );
}

export default MyApp;

3. pages/products/[id].js (동적 페이지 SEO 적용)

import Seo from '../../components/Seo';

function ProductDetailPage({ product }) {
  if (!product) {
    return <div>상품 정보를 찾을 수 없습니다.</div>;
  }

  const pageUrl = `https://example.com/products/${product.id}`;

  return (
    <>
      <Seo
        title={product.name}
        description={`최고의 상품, ${product.name}을(를) 특별한 가격에 만나보세요. ${product.summary}`}
        image={product.imageUrl}
        url={pageUrl}
      />
      
      <main>
        <h1>{product.name}</h1>
        <p>{product.description}</p>
        <img src={product.imageUrl} alt={product.name} />
      </main>
    </>
  );
}

export async function getServerSideProps(context) {
  const { id } = context.params;
  const res = await fetch(`https://api.example.com/products/${id}`);
  
  // 404 처리
  if (!res.ok) {
    return { notFound: true };
  }

  const product = await res.json();
  
  return {
    props: {
      product,
    },
  };
}

export default ProductDetailPage;

🔍 더 깊이 찾아보기

  1. next-sitemap: 빌드 시점에 sitemap.xmlrobots.txt를 자동으로 생성해주는 라이브러리. postbuild 스크립트에 next-sitemap을 추가하여 자동화할 수 있음. 검색 엔진이 사이트의 모든 페이지를 효율적으로 크롤링하도록 돕는 필수 과정.

  2. JSON-LD (구조화된 데이터): 검색 엔진에게 페이지 콘텐츠의 의미(예: 이 페이지는 '상품'에 대한 정보이며, 가격은 '얼마'이고, 평점은 '몇 점'이다)를 명확하게 전달하는 표준 데이터 형식. 검색 결과에서 별점, 가격 등 '리치 스니펫'으로 노출될 확률을 높여줌.

    • Seo 컴포넌트에 다음과 같이 추가할 수 있음:
    // Seo 컴포넌트 내부에 추가
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify({
          '@context': 'https://schema.org',
          '@type': 'Product',
          name: product.name,
          image: product.imageUrl,
          description: product.summary,
          sku: product.id,
          offers: {
            '@type': 'Offer',
            url: pageUrl,
            priceCurrency: 'KRW',
            price: product.price,
            availability: 'https://schema.org/InStock',
          },
        }),
      }}
      key="product-jsonld" // key를 통해 중복 방지
    />
profile
더 이상 미룰 수 없다 나의 공부 나의 성장

0개의 댓글