전통적인 SPA(Single Page Application)는 클라이언트 사이드에서 document.title 등을 조작하여 SEO에 취약했음. Next.js는 서버에서 페이지를 렌더링하는 시점에 각 페이지에 최적화된 <head> 정보를 미리 HTML에 삽입함. next/head 컴포넌트는 이 과정을 직관적으로 만들어, 페이지별 동적 title, meta 태그 관리를 가능하게 하고 검색 엔진 최적화(SEO)를 극대화하는 핵심 도구임.
next/head의 작동 원리next/head는 React 컴포넌트 트리 어디에나 위치할 수 있음. Next.js는 렌더링 시점에 모든 next/head 컴포넌트의 자식들을 수집(collect)하여 최종 HTML의 <head> 태그 안에 주입함.
name이나 property를 가진 meta 태그가 여러 번 선언되면, 컴포넌트 트리상 가장 마지막(가장 깊은 곳)에 선언된 태그만 렌더링됨. 이를 통해 공통 레이아웃에 설정된 기본값을 페이지별로 쉽게 덮어쓸 수 있음.key Prop의 중요성: title, meta viewport처럼 페이지에 유일해야 하는 태그들은 Next.js가 자동으로 중복을 처리함. 하지만 og:image처럼 여러 개가 존재할 수 있는 태그를 유일하게 만들고 싶거나, 재정의 순서를 명확히 제어하고 싶을 때 key prop을 사용함. key가 동일한 태그는 하나만 렌더링됨.title: 검색 결과 최상단, 브라우저 탭에 노출. 사용자의 첫인상을 결정. (권장 길이: 50-60자)meta name="description": 검색 결과의 요약문. 클릭률(CTR)에 직접적인 영향을 줌. (권장 길이: 150-160자)meta name="viewport": 모바일 기기에서의 화면 렌더링 방식을 제어. width=device-width, initial-scale=1은 필수.og:): 소셜 미디어 공유 시 표시될 콘텐츠(제목, 설명, 이미지, URL)를 정의.link rel="canonical": 여러 URL이 동일한 콘텐츠를 가리킬 때, 검색 엔진에 어떤 URL이 '대표'인지 알려주어 중복 콘텐츠 페널티를 방지.효율적인 관리를 위해 컴포넌트 계층에 따라 Head를 설정하는 것이 좋음.
_app.js (전역 기본값): 사이트 전체에 적용될 기본 title (또는 title 템플릿), viewport, 기본 og:site_name 등을 설정. 모든 페이지의 기반이 됨.Layout 컴포넌트 (페이지 유형별 기본값): 블로그, 상품 목록 등 특정 페이지 유형에 공통으로 적용될 head 정보를 설정._app.js나 Layout의 기본값을 덮어씀. 예를 들어, 상품 상세 페이지의 상품명, 상품 이미지 등을 동적으로 설정.공통 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;
next-sitemap: 빌드 시점에 sitemap.xml과 robots.txt를 자동으로 생성해주는 라이브러리. postbuild 스크립트에 next-sitemap을 추가하여 자동화할 수 있음. 검색 엔진이 사이트의 모든 페이지를 효율적으로 크롤링하도록 돕는 필수 과정.
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를 통해 중복 방지
/>