
Next.js SEO 최적화: 메타데이터부터 Core Web Vitals까지
자주 묻는 질문
Q. Next.js는 왜 SEO에 유리한가요?
A. Next.js는 SSR(서버 사이드 렌더링)과 SSG(정적 생성)를 지원해 검색엔진 크롤러가 완성된 HTML을 즉시 수집할 수 있습니다. CRA 같은 SPA는 자바스크립트 실행 후에야 콘텐츠가 생성되어 크롤링에 불리합니다.
Q. App Router에서 페이지별 메타데이터는 어떻게 설정하나요?
A. 정적 페이지는 layout.tsx 또는 page.tsx에서 metadata 객체를 export해 선언합니다. 블로그 포스트·상품 페이지처럼 동적 라우트는 generateMetadata() 함수를 사용해 페이지별로 title, description, OG 태그를 다르게 적용할 수 있습니다.
Q. Core Web Vitals의 '양호' 기준값은 무엇인가요?
A. Google 권장 기준은 LCP 2.5초 이하, CLS 0.1 이하, INP 200ms 이하입니다. 세 지표 모두 양호 범위에 들어야 검색 순위에 긍정적인 영향을 미칩니다.
Q. next/image를 사용하면 SEO에 어떤 도움이 되나요?
A. next/image는 WebP 자동 변환, 지연 로딩, srcset 자동 생성으로 이미지 로딩 성능을 개선합니다. 히어로 이미지에 priority 속성을 추가하면 LCP 점수를 직접 향상시킬 수 있습니다.
Q. 구조화 데이터(Schema Markup)를 적용하면 실제로 효과가 있나요?
A. 구조화 데이터를 올바르게 적용하면 Google 검색 결과에 별점, FAQ, 브레드크럼 같은 리치 스니펫이 노출되어 CTR이 평균 20~30% 향상되는 효과가 보고됩니다. Google Rich Results Test로 유효성을 반드시 검증하세요.
Next.js가 SEO에 유리한 이유
Next.js 프로젝트를 배포했는데 Google에서 검색이 되지 않는다는 경험, 혹시 있으신가요? Create React App(CRA)으로 만든 서비스가 검색 순위에서 좀처럼 오르지 않아 고민하셨다면, 그 원인은 렌더링 방식에 있을 가능성이 높습니다.
CRA 같은 전통적인 SPA(Single Page Application)는 클라이언트 사이드 렌더링(CSR) 방식으로 동작합니다. 사용자의 브라우저에서 자바스크립트가 실행된 뒤에야 비로소 화면 콘텐츠가 생성되는 구조입니다. 문제는 Google 크롤러가 자바스크립트를 실행하는 데 시간이 걸리거나, 경우에 따라 실행 자체를 건너뛴다는 점입니다. 크롤러가 빈 HTML만 수집해 가면, 아무리 좋은 콘텐츠라도 검색 결과에 제대로 반영되지 않습니다.
Next.js는 이 문제를 구조적으로 해결합니다. SSR(서버 사이드 렌더링)은 요청 시점에 서버에서 완성된 HTML을 생성해 크롤러에게 전달하고, SSG(정적 생성)는 빌드 시점에 HTML을 미리 만들어 둡니다. 두 방식 모두 크롤러가 자바스크립트 실행 없이도 전체 콘텐츠를 즉시 수집할 수 있어 색인 속도와 품질이 크게 향상됩니다.
App Router와 Pages Router의 SEO 관점 차이도 짚어볼 필요가 있습니다. Pages Router는 _document.tsx와 next/head를 조합해 메타데이터를 관리했습니다. App Router는 metadata 객체와 generateMetadata() 함수를 통해 파일 시스템 기반으로 메타데이터를 선언적으로 관리할 수 있습니다. 중복 선언 없이 레이아웃에서 페이지로 상속되는 구조라, 유지보수 측면에서도 App Router가 명확하게 유리합니다.
메타데이터·OG 태그·사이트맵 한 번에 잡기
정적·동적 메타데이터 설정
App Router에서 정적 메타데이터는 layout.tsx 또는 page.tsx에서 metadata 객체를 export하는 방식으로 선언합니다.
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: {
default: '위커프 — 업무 자동화 SaaS',
template: '%s | 위커프',
},
description: 'IT 스타트업을 위한 업무 자동화 플랫폼',
alternates: {
canonical: 'https://weekerp.com',
},
};
블로그 포스트나 상품 페이지처럼 동적 라우트에는 generateMetadata() 함수를 사용합니다. 이 함수는 URL 파라미터나 외부 API 데이터를 기반으로 페이지마다 다른 메타데이터를 생성할 수 있습니다.
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: { slug: string };
}): Promise<Metadata> {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.summary,
alternates: {
canonical: `https://weekerp.com/blog/${params.slug}`,
},
openGraph: {
title: post.title,
description: post.summary,
images: [{ url: post.thumbnail, width: 1200, height: 630 }],
type: 'article',
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.summary,
images: [post.thumbnail],
},
};
}
alternates.canonical은 놓치기 쉬운 설정이지만 반드시 챙겨야 합니다. 쿼리 파라미터(?page=1, ?sort=latest)가 붙은 URL이 다수 존재할 때, canonical이 없으면 Google이 동일 콘텐츠를 여러 URL로 인식해 색인 점수가 분산됩니다. 모든 동적 페이지에 canonical을 명시적으로 선언하는 습관을 들이세요.
사이트맵과 robots.txt 자동화
App Router는 app/sitemap.ts와 app/robots.ts 파일만 생성하면 Next.js가 자동으로 해당 경로(/sitemap.xml, /robots.txt)를 처리합니다.
// app/sitemap.ts
import { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await fetchAllPosts(); // DB 또는 CMS에서 포스트 목록 조회
const postEntries = posts.map((post) => ({
url: `https://weekerp.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.8,
}));
return [
{ url: 'https://weekerp.com', priority: 1.0 },
{ url: 'https://weekerp.com/blog', priority: 0.9 },
...postEntries,
];
}
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const isProduction = process.env.NODE_ENV === 'production';
return {
rules: {
userAgent: '*',
allow: isProduction ? '/' : undefined,
disallow: isProduction ? ['/admin/', '/api/'] : '/',
},
sitemap: 'https://weekerp.com/sitemap.xml',
};
}
스테이징 환경에서 disallow: '/'로 전체 크롤링을 차단하는 로직은 실수로 스테이징 URL이 Google에 색인되는 상황을 방지합니다. 사이트맵을 생성했다면 Google Search Console의 [사이트맵] 메뉴에 URL을 등록하고, 제출 후 색인 상태를 주기적으로 확인하는 것을 권장합니다.
이미지 SEO와 Core Web Vitals 실전 최적화
next/image로 LCP 직접 개선하기
next/image는 사용하는 것만으로도 WebP 자동 변환, 지연 로딩(lazy loading), srcset 자동 생성을 제공합니다. 그러나 히어로 이미지(페이지 최상단에 보이는 대형 이미지)에는 반드시 priority 속성을 추가해야 합니다.
// 히어로 이미지: priority 필수
<Image
src="/hero-banner.webp"
alt="위커프 업무 자동화 플랫폼 메인 배너"
width={1200}
height={630}
priority // preload 처리 → LCP 직접 개선
/>
// 스크롤 아래 이미지: 기본값(lazy) 유지
<Image
src="/feature-screenshot.webp"
alt="자동화 워크플로우 설정 화면"
width={800}
height={500}
/>
priority가 없으면 브라우저가 해당 이미지를 늦게 로드하고, 이는 LCP(최대 콘텐츠풀 페인트) 점수를 직접 낮춥니다. Google PageSpeed Insights에서 "LCP 요소를 미리 로드하세요" 경고가 뜬다면 십중팔구 이 설정이 누락된 경우입니다.
alt 텍스트는 SEO 키워드를 억지로 삽입하는 대신, 이미지가 실제로 무엇을 보여주는지 자연스럽게 설명해야 합니다. alt="위커프 SEO 자동화 업무 자동화 플랫폼 SaaS Next.js" 같은 키워드 나열은 Google이 스팸으로 간주할 수 있습니다. alt="자동화 워크플로우 설정 화면" 수준의 간결하고 정확한 설명이 적절합니다.
외부 이미지 도메인을 사용할 경우 next.config.js에서 허용 도메인을 명시해야 합니다.
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.weekerp.com',
pathname: '/images/**',
},
],
},
};
와일드카드(hostname: '**')로 모든 외부 도메인을 허용하는 설정은 보안 위험이 있으므로 반드시 필요한 도메인만 명시하세요.
CLS와 INP 개선 전략
CLS(누적 레이아웃 이동)는 페이지 로드 중 요소가 이동하는 정도를 측정합니다. next/image는 width와 height를 명시하면 브라우저가 이미지 공간을 미리 확보해 레이아웃 이동을 방지합니다. 광고 영역, 배너, 동적으로 삽입되는 콘텐츠에도 고정 크기를 지정하는 것이 CLS 개선의 핵심입니다. 웹폰트를 사용한다면 font-display: swap을 설정해 폰트 로딩 중 레이아웃이 흔들리는 현상을 줄이세요.
INP(다음 페인트까지의 상호작용)는 사용자가 클릭이나 입력을 했을 때 화면이 반응하는 속도를 측정합니다. 이벤트 핸들러 안에서 무거운 연산을 동기적으로 처리하면 INP가 급격히 나빠집니다. React 18의 startTransition을 활용해 긴급하지 않은 상태 업데이트를 후순위로 처리하거나, 무거운 작업은 Web Worker로 분리하는 것을 검토해 보세요.
측정은 PageSpeed Insights에서 실제 URL을 입력해 필드 데이터와 랩 데이터를 함께 확인하거나, Chrome DevTools의 Performance 패널에서 병목 구간을 직접 분석할 수 있습니다.
구조화 데이터로 리치 스니펫 노리기
구조화 데이터(Schema Markup)는 Google이 페이지 내용을 더 깊이 이해하도록 돕는 추가 신호입니다. 올바르게 적용하면 검색 결과에 별점, FAQ 아코디언, 브레드크럼 같은 리치 스니펫이 노출되어 CTR이 평균 20~30% 향상되는 효과가 보고됩니다.
Next.js에서 JSON-LD를 삽입하는 가장 깔끔한 방법은 <script> 태그를 직접 JSX에 삽입하는 방식입니다.
// app/blog/[slug]/page.tsx
export default function BlogPost({ post }) {
const jsonLd = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.summary,
author: {
'@type': 'Organization',
name: '위커프',
url: 'https://weekerp.com',
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
image: post.thumbnail,
};
return (
<>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
/>
{/* 본문 */}
</>
);
}
자주 활용되는 스키마 타입은 다음과 같습니다.
-
Article: 블로그 포스트, 뉴스 기사 — 발행일, 수정일, 저자 정보 포함
-
BreadcrumbList: 탐색 경로 표시 — 검색 결과 URL 아래 계층 구조 노출
-
FAQPage: 자주 묻는 질문 — 검색 결과에서 아코디언 형식으로 확장 표시
-
Product: 상품 페이지 — 가격, 재고, 별점 리치 스니펫
마크업을 작성한 뒤에는 반드시 Google Rich Results Test에서 유효성을 검증하세요. 스키마 타입이 지원되는지, 필수 필드가 누락되지 않았는지 즉시 확인할 수 있습니다. 유효하지 않은 마크업은 리치 스니펫 노출이 거부되므로, 배포 전 검증은 필수입니다.
배포 전 Next.js SEO 체크리스트
아래 항목을 배포 전 순서대로 점검하면 주요 SEO 실수를 사전에 방지할 수 있습니다.
메타데이터
-
모든 페이지에 고유한
title과description적용 여부 확인 -
generateMetadata()로 동적 라우트 페이지별 메타데이터 분리 -
OG 태그(
og:title,og:description,og:image) 설정 및 SNS 미리보기 확인 -
모든 페이지에 canonical URL 명시 — 쿼리 파라미터로 인한 중복 콘텐츠 방지
-
검색 결과에 노출되지 않아야 할 페이지(
/admin,/thank-you등)에noindex설정
사이트맵·크롤링
-
app/sitemap.ts로 동적 사이트맵 생성 및/sitemap.xml접근 확인 -
app/robots.ts에서 스테이징 환경disallow: '/'적용 여부 점검 -
Google Search Console에 사이트맵 등록 및 색인 오류 없음 확인
이미지
-
모든
<Image>컴포넌트에 의미 있는alt텍스트 적용 -
히어로 이미지(Above-the-fold)에
priority속성 추가 -
외부 이미지 도메인은
next.config.js에 최소 범위로 명시
Core Web Vitals
-
LCP < 2.5s: PageSpeed Insights에서 히어로 이미지 프리로드 경고 없음
-
CLS < 0.1: 이미지·광고 영역 크기 명시, 폰트 스왑 처리 완료
-
INP < 200ms: 무거운 이벤트 핸들러 분리 또는 Transition API 적용 확인
구조화 데이터
-
블로그·상품·FAQ 페이지에 JSON-LD 스키마 삽입
-
Google Rich Results Test 통과 확인
결론
Next.js SEO 최적화는 단일 설정 하나로 완성되지 않습니다. SSR·SSG로 크롤러에게 완성된 HTML을 제공하는 렌더링 전략, generateMetadata()로 페이지마다 정확한 메타데이터를 선언하는 것, next/image의 priority 속성으로 LCP를 직접 개선하는 것, 그리고 구조화 데이터로 리치 스니펫 기회를 노리는 것까지 — 각 레이어를 체계적으로 쌓아야 실질적인 검색 순위 상승으로 이어집니다.
지금 당장 실행할 수 있는 첫 번째 액션은 PageSpeed Insights에 서비스 URL을 입력해 Core Web Vitals 세 지표를 확인하는 것입니다. LCP·CLS·INP 중 '개선 필요' 범위에 있는 지표부터 이 가이드의 해당 섹션을 참고해 하나씩 개선해 나가세요. 기술적 SEO 기반이 탄탄해지면, 좋은 콘텐츠가 검색 결과에서 제 실력을 발휘할 수 있습니다.



