Next.js SEO 최적화 실전 테크닉
Next.js의 강력한 SEO 기능을 제대로 활용하고 계신가요? App Router 기반의 메타데이터 관리부터 구조화 데이터, 사이트맵 자동화까지.

읽기 전에 핵심만
- Next.js SEO는 메타데이터, 구조화 데이터, 크롤링 파일을 페이지 구조와 함께 관리해야 합니다.
- 프레임워크 설정만 믿지 말고 배포된 문서의 head와 구조화 데이터, 크롤링 경로를 직접 검증하세요.
- 검색 최적화는 일회성 설정이 아니라 콘텐츠 변경과 배포 흐름에 포함할 지속적인 품질 점검입니다.
Next.js는 React 기반 프레임워크 중 가장 SEO 친화적인 선택입니다. 하지만 프레임워크가 SEO를 '가능하게' 해줄 뿐, 실제 최적화는 개발자의 몫입니다.
이 가이드에서는 Next.js 15 App Router 기준으로 실전에서 바로 적용 가능한 SEO 최적화 테크닉을 다룹니다.
Next.js SEO의 3가지 핵심 축
1. 기술적 SEO (Technical SEO)
- 서버 사이드 렌더링 (SSR/SSG)
- Core Web Vitals 최적화
- 구조화 데이터 (JSON-LD)
- Sitemap/Robots.txt 자동화
2. On-Page SEO
- 메타데이터 최적화
- 시맨틱 HTML 구조
- 내부 링크 전략
- 이미지 최적화
3. 콘텐츠 SEO
- 키워드 전략
- 사용자 의도 반영
- E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness)
1. 메타데이터 최적화: App Router 방식
Next.js 15의 App Router는 파일 기반 메타데이터 시스템을 제공합니다.
정적 메타데이터 (기본)
// app/about/page.tsx
import { Metadata } from 'next';
export const metadata: Metadata = {
title: 'About POEMA | Digital Agency',
description: 'POEMA는 웹 개발, 이커머스, 퍼포먼스 마케팅을 제공하는 디지털 에이전시입니다.',
keywords: ['웹 개발', '이커머스', '디지털 마케팅', 'POEMA'],
authors: [{ name: 'POEMA Team' }],
openGraph: {
title: 'About POEMA',
description: 'Digital transformation partner for startups',
images: ['/og-image.jpg'],
type: 'website',
},
twitter: {
card: 'summary_large_image',
title: 'About POEMA',
description: 'Digital transformation partner',
images: ['/twitter-image.jpg'],
},
};
export default function AboutPage() {
return <div>...</div>;
}동적 메타데이터 (추천)
케이스 스터디, 블로그 포스트 등 동적 페이지에 필수입니다.
// app/portfolio/[slug]/page.tsx
import { Metadata } from 'next';
import { getCaseStudy } from '@/data/marketing/case-studies';
export async function generateMetadata({
params
}: {
params: Promise<{ slug: string }>
}): Promise<Metadata> {
const { slug } = await params;
const caseStudy = await getCaseStudy(slug);
if (!caseStudy) {
return {
title: 'Portfolio Not Found',
};
}
return {
title: `${caseStudy.title.ko} | POEMA Portfolio`,
description: caseStudy.overview.ko,
keywords: caseStudy.tags.ko,
openGraph: {
title: caseStudy.title.ko,
description: caseStudy.overview.ko,
images: [
{
url: caseStudy.thumbnail,
width: 1200,
height: 630,
alt: caseStudy.title.ko,
},
],
type: 'article',
publishedTime: caseStudy.publishedAt,
},
};
}2. 구조화 데이터: JSON-LD로 리치 스니펫 획득
구조화 데이터는 검색 엔진이 콘텐츠를 이해하도록 돕고, 리치 스니펫을 통해 CTR을 높입니다.
Organization Schema (전역)
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
const organizationSchema = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: 'POEMA',
url: 'https://poema.agency',
logo: 'https://poema.agency/logo.png',
description: 'Digital agency specializing in web development and e-commerce',
address: {
'@type': 'PostalAddress',
addressCountry: 'KR',
addressLocality: 'Seoul',
},
contactPoint: {
'@type': 'ContactPoint',
telephone: '+82-2-1234-5678',
contactType: 'Customer Service',
email: 'hello@poema.agency',
},
sameAs: [
'https://www.instagram.com/poema.agency',
'https://www.linkedin.com/company/poema',
],
};
return (
<html>
<head>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(organizationSchema) }}
/>
</head>
<body>{children}</body>
</html>
);
}Article Schema (블로그 포스트)
// components/seo/BlogPostJsonLd.tsx
interface BlogPostJsonLdProps {
title: string;
description: string;
publishedAt: string;
author: string;
image: string;
url: string;
}
export function BlogPostJsonLd({ title, description, publishedAt, author, image, url }: BlogPostJsonLdProps) {
const schema = {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: title,
description: description,
image: image,
datePublished: publishedAt,
author: {
'@type': 'Person',
name: author,
},
publisher: {
'@type': 'Organization',
name: 'POEMA',
logo: {
'@type': 'ImageObject',
url: 'https://poema.agency/logo.png',
},
},
mainEntityOfPage: {
'@type': 'WebPage',
'@id': url,
},
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}FAQPage Schema
export function FAQJsonLd({ faqs }: { faqs: { question: string; answer: string }[] }) {
const schema = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: faqs.map((faq) => ({
'@type': 'Question',
name: faq.question,
acceptedAnswer: {
'@type': 'Answer',
text: faq.answer,
},
})),
};
return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}3. Sitemap & Robots.txt 자동화

Next.js 15는 파일 기반 sitemap/robots 생성을 지원합니다.
동적 Sitemap 생성
// app/sitemap.ts
import { MetadataRoute } from 'next';
import { caseStudies } from '@/data/marketing/case-studies';
import { blogPosts } from '@/data/marketing/blog';
export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://poema.agency';
// Static pages
const staticPages: MetadataRoute.Sitemap = [
{
url: baseUrl,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 1.0,
},
{
url: `${baseUrl}/services`,
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: `${baseUrl}/portfolio`,
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.9,
},
];
// Dynamic case studies
const caseStudyPages: MetadataRoute.Sitemap = caseStudies.map((study) => ({
url: `${baseUrl}/portfolio/${study.slug}`,
lastModified: new Date(study.publishedAt),
changeFrequency: 'monthly' as const,
priority: 0.7,
}));
// Dynamic blog posts
const blogPages: MetadataRoute.Sitemap = blogPosts.map((post) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.publishedAt),
changeFrequency: 'monthly' as const,
priority: 0.6,
}));
return [...staticPages, ...caseStudyPages, ...blogPages];
}Robots.txt 설정
// app/robots.ts
import { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
const baseUrl = 'https://poema.agency';
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/_next/'],
},
{
userAgent: 'Googlebot',
allow: '/',
disallow: ['/api/', '/admin/'],
},
],
sitemap: `${baseUrl}/sitemap.xml`,
};
}4. Core Web Vitals 최적화
Google은 LCP, FID, CLS를 페이지 경험 지표로 평가합니다.
LCP (Largest Contentful Paint) 최적화
목표: 2.5초 이하
// 이미지 최적화
import Image from 'next/image';
<Image
src="/hero.jpg"
alt="Hero image"
width={1920}
height={1080}
priority // LCP 이미지에 필수
quality={90}
placeholder="blur"
blurDataURL="data:image/..."
/>
// 폰트 최적화
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // FOUT 방지
preload: true,
});CLS (Cumulative Layout Shift) 최적화
목표: 0.1 이하
// 이미지/동영상에 명시적 크기 지정
<Image src="..." width={600} height={400} alt="..." />
// 동적 콘텐츠에 최소 높이 설정
<div className="min-h-[400px]">
{isLoading ? <Skeleton /> : <Content />}
</div>
// 폰트 preload로 FOUT 방지
<link rel="preload" href="/fonts/custom.woff2" as="font" type="font/woff2" crossOrigin="anonymous" />FID/INP 최적화
목표: FID 100ms 이하, INP 200ms 이하
- Dynamic import로 JS 번들 분할
- 무거운 컴포넌트는 below-the-fold에서만 로드
- debounce/throttle로 이벤트 핸들러 최적화
5. 다국어 SEO (next-intl 활용)
// app/[locale]/layout.tsx
import { Metadata } from 'next';
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
return {
alternates: {
canonical: `https://poema.agency/${locale}`,
languages: {
'ko-KR': 'https://poema.agency/ko',
'en-US': 'https://poema.agency/en',
},
},
};
}Hreflang 태그는 Next.js가 자동 생성하지만, Sitemap에는 수동 추가 필요:
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://poema.agency/ko',
lastModified: new Date(),
alternates: {
languages: {
en: 'https://poema.agency/en',
},
},
},
];
}6. SEO 체크리스트: 론칭 전 필수 확인
기술적 체크
| 항목 | 도구 | 목표 |
|---|---|---|
| Core Web Vitals | PageSpeed Insights | LCP <2.5s, FID <100ms, CLS <0.1 |
| 모바일 친화성 | Mobile-Friendly Test | Pass |
| 구조화 데이터 | Rich Results Test | 0 Errors |
| 보안 | SSL Labs | A+ |
| Sitemap | Google Search Console | Indexed |
On-Page 체크
자주 묻는 질문
Next.js App Router에서는 페이지 메타데이터를 어떻게 관리하나요?
정적인 값은 metadata 객체로, 경로별 데이터가 필요한 값은 generateMetadata로 정의하고 실제 렌더링 결과의 head를 확인하세요.
JSON-LD는 Next.js에서 어디에 넣는 것이 좋나요?
해당 엔터티를 보여 주는 layout 또는 page 컴포넌트에서 구조화 데이터를 스크립트로 렌더링하고, 외부 입력은 안전하게 처리한 뒤 검증 도구로 확인하세요.
sitemap과 robots 파일만 만들면 SEO 설정이 끝나나요?
아닙니다. 두 파일은 크롤링과 URL 발견을 돕는 요소이며, 페이지별 메타데이터와 콘텐츠 품질, 내부 링크, 접근 가능한 렌더링도 함께 점검해야 합니다.


