블로그TECH NOTE
개발

Next.js SEO 최적화 실전 테크닉

이 글에서 해결할 것

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

POEMORA · 편집팀2025-06-0212
페이지 콘텐츠와 메타데이터, 구조 관계, 크롤링 경로가 여러 층으로 연결된 기술 파이프라인
검색 최적화는 개별 태그가 아니라 렌더링과 발견, 이해를 잇는 시스템으로 다뤄야 합니다.
#Next.js#SEO#메타데이터#구조화 데이터#성능 최적화

읽기 전에 핵심만

  1. Next.js SEO는 메타데이터, 구조화 데이터, 크롤링 파일을 페이지 구조와 함께 관리해야 합니다.
  2. 프레임워크 설정만 믿지 말고 배포된 문서의 head와 구조화 데이터, 크롤링 경로를 직접 검증하세요.
  3. 검색 최적화는 일회성 설정이 아니라 콘텐츠 변경과 배포 흐름에 포함할 지속적인 품질 점검입니다.

Next.js는 React 기반 프레임워크 중 가장 SEO 친화적인 선택입니다. 하지만 프레임워크가 SEO를 '가능하게' 해줄 뿐, 실제 최적화는 개발자의 몫입니다.

이 가이드에서는 Next.js 15 App Router 기준으로 실전에서 바로 적용 가능한 SEO 최적화 테크닉을 다룹니다.

Next.js SEO의 3가지 핵심 축

SEO Optimization Pyramid Authority Backlinks & Trust Content Strategy Keywords, Quality, UX Technical SEO Backlinks, Reviews, Brand Mentions Keyword Research, Blog Posts, Landing Pages Core Web Vitals, Schema, Sitemap
SEO is built layer by layer from technical foundation to authority

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는 파일 기반 메타데이터 시스템을 제공합니다.

Next.js SEO Metadata Flow generateMetadata Server Function Runs at request/build HTML <head> title, meta, OG tags Injected into response Search Crawler Google, Bing, etc. Reads & indexes Search Results Rich snippets, OG cards Shown to users
How metadata flows from code to search results

정적 메타데이터 (기본)

typescript
// 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>;
}

동적 메타데이터 (추천)

케이스 스터디, 블로그 포스트 등 동적 페이지에 필수입니다.

typescript
// 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 (전역)

typescript
// 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 (블로그 포스트)

typescript
// 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

typescript
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 자동화

연결된 웹사이트 노드 사이에서 탐색 경로와 접근 제어 통로가 나뉘는 모습을 표현한 지도
사이트맵은 발견 경로를 제공하고 robots 설정은 크롤러가 접근할 범위를 전달합니다.

Next.js 15는 파일 기반 sitemap/robots 생성을 지원합니다.

동적 Sitemap 생성

typescript
// 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 설정

typescript
// 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 최적화

Core Web Vitals Targets LCP Largest Contentful Paint ≤ 2.5s Loading Speed INP Interaction to Next Paint ≤ 200ms Responsiveness CLS Cumulative Layout Shift ≤ 0.1 Visual Stability
Google uses these metrics as ranking signals

Google은 LCP, FID, CLS를 페이지 경험 지표로 평가합니다.

LCP (Largest Contentful Paint) 최적화

목표: 2.5초 이하

typescript
// 이미지 최적화
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 이하

tsx
// 이미지/동영상에 명시적 크기 지정
<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 활용)

typescript
// 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에는 수동 추가 필요:

typescript
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 VitalsPageSpeed InsightsLCP <2.5s, FID <100ms, CLS <0.1
모바일 친화성Mobile-Friendly TestPass
구조화 데이터Rich Results Test0 Errors
보안SSL LabsA+
SitemapGoogle Search ConsoleIndexed

On-Page 체크

FAQ

자주 묻는 질문

Next.js App Router에서는 페이지 메타데이터를 어떻게 관리하나요?

정적인 값은 metadata 객체로, 경로별 데이터가 필요한 값은 generateMetadata로 정의하고 실제 렌더링 결과의 head를 확인하세요.

JSON-LD는 Next.js에서 어디에 넣는 것이 좋나요?

해당 엔터티를 보여 주는 layout 또는 page 컴포넌트에서 구조화 데이터를 스크립트로 렌더링하고, 외부 입력은 안전하게 처리한 뒤 검증 도구로 확인하세요.

sitemap과 robots 파일만 만들면 SEO 설정이 끝나나요?

아닙니다. 두 파일은 크롤링과 URL 발견을 돕는 요소이며, 페이지별 메타데이터와 콘텐츠 품질, 내부 링크, 접근 가능한 렌더링도 함께 점검해야 합니다.

REFERENCES

확인한 자료

관련 글