SOYOYU
블로그로 돌아가기기술 SEO

Next.js App Router SEO 체크리스트: RSC, 하이드레이션, 코드 스플리팅 점검

Next.js App Router 사이트에서 실제로 발생하는 SEO 문제를 체크리스트로 정리합니다. RSC 스트리밍, 하이드레이션 불일치, searchParams 함정, 네이버 대응까지 코드와 함께 점검하세요.

소요유2026년 4월 30일13 min read
Next.js SEOApp RouterReact Server Components기술 SEOSEO 체크리스트

TL;DR - 핵심 요약

  • generateMetadata에서 fetch 실패 시 빈 title이 렌더링될 수 있음 — fallback 메타데이터 설정 필수
  • searchParams 사용 시 동적 렌더링으로 전환될 수 있음 — PPR의 Suspense 경계를 활용하면 정적 셸 유지 가능
  • 존재하지 않는 URL에서 200 반환하는 소프트 404 문제는 Next.js 사이트에서 흔히 발생
  • Client Components('use client')도 서버에서 프리렌더링됨 — 초기 HTML에 콘텐츠 포함
  • 하이드레이션 불일치 시 React가 DOM을 재구축할 수 있음 — 성능과 SEO에 부정적 영향
  • 네이버는 JS 렌더링을 지원하지만 효율이 낮아 SSR을 공식 권장, # 프래그먼트 URL은 수집 시 제거
  • Next.js 14에서 도입된 PPR(Partial Prerendering, experimental)은 정적/동적 렌더링의 장점을 결합

이 체크리스트의 목적

Next.js App Router를 사용한다고 SEO가 자동으로 해결되지 않습니다. 오히려 App Router 특유의 렌더링 모델(RSC, 스트리밍, PPR)을 이해하지 않으면 SEO에 해로운 설정을 모르고 적용하는 경우가 많습니다.

이 글은 Next.js App Router 프로젝트에서 실제로 발생하는 SEO 문제를 체크리스트 형태로 정리합니다. 각 항목은 코드 예시와 함께 확인할 수 있도록 구성했습니다.


1. 메타데이터와 상태 코드 점검

체크 1-1: generateMetadata fetch 실패 시 fallback 처리

generateMetadata에서 비동기 데이터를 가져올 때, API 응답 실패나 타임아웃이 발생하면 title이 undefined로 렌더링됩니다. Search Console에서는 이 페이지들이 중복 title 이슈로 보고됩니다.

// 위험한 패턴 — fetch 실패 시 빈 title
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug); // 실패 시 title이 undefined
  return { title: post.title };
}

// 안전한 패턴 — fallback 포함
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug).catch(() => null);
  return {
    title: post?.title ?? '기본 페이지 제목 | 사이트명',
    description: post?.excerpt ?? '기본 설명',
  };
}
  • generateMetadata에서 fetch 호출에 .catch() 또는 try-catch 적용 여부
  • fallback title이 사이트명을 포함하는 의미 있는 값인지
  • Search Console "페이지" 리포트에서 "중복 제목" 이슈 확인

체크 1-2: notFound()를 통한 404 상태 코드 반환

많은 Next.js 사이트에서 존재하지 않는 URL이 200 상태 코드를 반환합니다. 동적 라우트에서 데이터가 없을 때 notFound()를 호출하지 않으면, 빈 페이지가 200으로 제공되어 Google이 이를 색인하거나 소프트 404로 처리합니다.

출처: PPC Land - Google Clarifies JavaScript Rendering for Error Pages

Google 2025년 12월 업데이트: 비200 상태 코드 페이지는 렌더링 파이프라인에서 제외될 수 있음

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetchPost(slug);

  if (!post) {
    notFound(); // 404 상태 코드 반환 — Google은 렌더링 건너뜀
  }

  return <article>{/* ... */}</article>;
}
  • 동적 라우트([slug], [id])에서 데이터 없을 때 notFound() 호출 여부
  • curl -I https://yoursite.com/존재하지않는경로로 실제 상태 코드 확인
  • not-found.tsx 파일에 사용자 친화적 404 페이지 구현 여부

체크 1-3: canonical URL 설정

App Router에서 canonical은 generateMetadataalternates 필드로 설정합니다. 누락하면 쿼리 파라미터가 붙은 URL이 별도 페이지로 색인될 수 있습니다.

export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  return {
    alternates: {
      canonical: `https://yoursite.com/blog/${slug}`,
    },
  };
}
  • 모든 페이지에 canonical URL이 설정되어 있는지
  • canonical URL이 절대 경로인지 (상대 경로는 해석 오류 가능)
  • 트레일링 슬래시 일관성 (/blog/post vs /blog/post/)

2. 렌더링 전략 점검

체크 2-1: searchParams가 정적 생성을 무력화하지 않는지

App Router에서 페이지 컴포넌트가 searchParams를 읽으면, 해당 페이지는 빌드 타임 정적 생성에서 제외(Static Generation)되고 매 요청마다 동적으로 렌더링됩니다. 이것은 조용히 발생하며, 빌드 로그를 주의 깊게 확인하지 않으면 놓치기 쉽습니다.

// 이 페이지는 동적 렌더링으로 전환됨
export default async function Page({
  searchParams,
}: {
  searchParams: Promise<{ q?: string }>;
}) {
  const { q } = await searchParams;
  // searchParams를 읽는 순간 정적 생성 불가
  return <div>검색어: {q}</div>;
}

검증 방법: npm run build 실행 후 출력에서 각 라우트의 렌더링 방식을 확인합니다.

Route (app)                    Size   First Load JS
┌ ○ /                          5.2 kB      89.5 kB
├ ○ /about                     1.8 kB      86.1 kB
├ λ /search                    3.1 kB      87.4 kB   ← λ = 동적
└ ● /blog/[slug]               2.4 kB      86.7 kB   ← ● = ISR/SSG
  • = 정적 (빌드 타임 생성)

  • = SSG/ISR (정적 + 재검증)

  • λ = 동적 (매 요청마다 서버 렌더링)

  • npm run build 출력에서 의도치 않은 λ(동적) 라우트가 없는지

  • searchParams가 필요한 페이지와 불필요한 페이지를 분리했는지

  • 검색/필터 페이지 외에 searchParams를 사용하는 곳이 없는지

체크 2-2: Client Components가 초기 HTML에 포함되는지 이해

'use client' 디렉티브를 붙인 Client Components도 서버에서 프리렌더링되어 초기 HTML에 콘텐츠가 포함됩니다. "클라이언트 컴포넌트는 크롤러가 못 본다"는 오해가 있지만, 실제로는 초기 HTML에 렌더링 결과가 포함됩니다. 다만 클라이언트에서 하이드레이션이 완료되어야 인터랙션이 활성화됩니다.

'use client';

// 이 컴포넌트의 렌더링 결과는 초기 HTML에 포함됨
export function ProductCard({ title, price }: { title: string; price: number }) {
  return (
    <div>
      <h3>{title}</h3>  {/* 크롤러가 볼 수 있음 */}
      <p>{price}원</p>  {/* 크롤러가 볼 수 있음 */}
      <button onClick={() => addToCart()}>장바구니</button>
    </div>
  );
}

하지만 useEffect 안에서만 렌더링되는 콘텐츠는 초기 HTML에 포함되지 않으므로 주의가 필요합니다.

'use client';

export function DynamicContent() {
  const [data, setData] = useState(null);
  
  useEffect(() => {
    fetch('/api/data').then(r => r.json()).then(setData);
  }, []);

  // data가 null인 상태의 HTML이 초기 렌더링에 포함됨
  // 실제 data 내용은 크롤러가 JS를 실행해야 보임
  if (!data) return <p>로딩 중...</p>;
  return <div>{data.content}</div>;
}
  • SEO에 중요한 콘텐츠가 useEffect 안에서만 렌더링되지 않는지
  • curl https://yoursite.com/페이지로 초기 HTML에 핵심 콘텐츠 포함 여부 확인
  • Client Components의 초기 렌더링 결과에 의미 있는 콘텐츠가 있는지

체크 2-3: RSC 스트리밍 콘텐츠의 크롤러 접근성

React Server Components의 스트리밍 렌더링은 Google이 정상적으로 처리합니다.

출처: Vercel - How Google Handles JavaScript Throughout the Indexing Process

Vercel/MERJ 연구: 200 상태 코드를 반환하는 HTML 페이지는 100% 렌더링 완료 — RSC 스트리밍 콘텐츠 포함

다만 Suspense boundary의 fallback 콘텐츠가 SEO에 중요한 정보를 담고 있지 않은지 확인해야 합니다. 스트리밍이 완료되기 전 크롤러가 HTML을 캡처하면 fallback이 색인될 수 있습니다.

// loading.tsx 또는 Suspense fallback에 의미 없는 텍스트 사용
<Suspense fallback={<div>로딩 중...</div>}>
  <ProductList /> {/* 핵심 콘텐츠 */}
</Suspense>
  • loading.tsx 파일이 SEO 핵심 페이지에 불필요하게 설정되어 있지 않은지
  • Suspense fallback에 "로딩 중" 같은 비정보성 텍스트만 있지 않은지
  • Google Search Console URL 검사 도구로 렌더링된 HTML 확인

3. 하이드레이션 점검

체크 3-1: 하이드레이션 불일치(Hydration Mismatch) 확인

하이드레이션 불일치는 서버에서 렌더링한 HTML과 클라이언트에서 React가 생성하는 DOM이 다를 때 발생합니다. React는 이를 버그로 취급하며, 경우에 따라 서버 렌더링 결과를 폐기하고 클라이언트에서 DOM을 재구축할 수 있습니다. 일부 불일치는 recoverable error로 자동 복구되지만, 심각한 경우 성능 저하와 CLS(Cumulative Layout Shift) 악화로 이어질 수 있습니다.

흔한 원인:

'use client';

// 1. Date/Time — 서버와 클라이언트의 시간대 차이
export function Timestamp() {
  // 서버: "2026-04-30T09:00:00Z" vs 클라이언트: "2026-04-30T18:00:00+09:00"
  return <time>{new Date().toLocaleDateString()}</time>;
}

// 2. window/document 접근
export function ResponsiveText() {
  // 서버에서는 window가 없어 다른 값 렌더링
  const isMobile = typeof window !== 'undefined' && window.innerWidth < 768;
  return <p>{isMobile ? '모바일' : '데스크톱'}</p>;
}

// 3. Math.random() 또는 ID 생성
export function UniqueItem() {
  return <div id={`item-${Math.random()}`}>내용</div>;
}

올바른 패턴:

'use client';

import { useState, useEffect } from 'react';

export function Timestamp({ serverTime }: { serverTime: string }) {
  const [displayTime, setDisplayTime] = useState(serverTime);

  useEffect(() => {
    // 클라이언트 시간은 하이드레이션 이후에 업데이트
    setDisplayTime(new Date().toLocaleDateString('ko-KR'));
  }, []);

  return <time>{displayTime}</time>;
}
  • 브라우저 콘솔에서 "Hydration failed" 또는 "Text content does not match" 경고 확인
  • Date, Math.random(), window 등 서버/클라이언트 불일치 소스 점검
  • Next.js dev 모드에서 하이드레이션 에러 리포트 활성화 여부

체크 3-2: suppressHydrationWarning 남용 여부

suppressHydrationWarning은 불일치 경고를 숨기지만 문제 자체를 해결하지 않습니다. 이 속성이 남용되면 실제 하이드레이션 문제가 숨겨져, 성능 저하와 CLS 문제가 감지되지 않습니다.

  • suppressHydrationWarning 사용 위치가 <time> 등 제한적인 요소에만 적용되었는지
  • 해당 속성을 적용한 이유가 코드 주석으로 문서화되어 있는지

4. 코드 스플리팅과 번들 점검

체크 4-1: 핵심 콘텐츠가 동적 import로 지연 로딩되지 않는지

next/dynamic이나 React.lazy로 핵심 콘텐츠를 지연 로딩하면, 크롤러가 해당 콘텐츠를 보지 못할 수 있습니다. 특히 Google 2025년 12월 업데이트 이후, 초기 HTML에 noindex가 있으면 JS 실행 자체가 차단될 수 있습니다.

출처: Search Engine Journal - Google Warns Noindex Can Block JavaScript

초기 HTML의 noindex는 JavaScript 실행을 차단할 수 있음

import dynamic from 'next/dynamic';

// 위험: SEO 핵심 콘텐츠를 dynamic import로 로딩
const ProductDescription = dynamic(() => import('./ProductDescription'), {
  ssr: false, // ssr: false는 서버 렌더링 자체를 비활성화
});

// 안전: 인터랙티브 요소만 dynamic import
const ReviewForm = dynamic(() => import('./ReviewForm'), {
  ssr: false, // 리뷰 작성 폼은 SEO 핵심 콘텐츠가 아님
});
  • dynamic(() => ..., { ssr: false })가 SEO 핵심 콘텐츠에 사용되지 않는지
  • ssr: false를 사용하는 모든 컴포넌트 목록 작성 및 SEO 영향 검토
  • 서드파티 위젯(채팅, 분석 등)만 ssr: false로 제한되어 있는지

체크 4-2: JavaScript 번들 크기와 로딩 성능

App Router에서는 Server Components의 JS가 클라이언트로 전송되지 않으므로 번들 크기가 줄어듭니다. 하지만 Client Components가 많아지면 번들 크기가 커져 LCP(Largest Contentful Paint)와 INP(Interaction to Next Paint)에 영향을 줍니다.

# Next.js 번들 분석
ANALYZE=true npm run build
# 또는 @next/bundle-analyzer 설정 후
npm run build
  • @next/bundle-analyzer를 통한 클라이언트 번들 크기 확인
  • 불필요한 'use client' 디렉티브가 상위 컴포넌트에 적용되어 있지 않은지
  • 서버에서만 필요한 라이브러리(DB 클라이언트, fs 등)가 클라이언트 번들에 포함되지 않는지

5. Next.js 15+ PPR(Partial Prerendering) 점검

Next.js 14.0에서 experimental로 도입되고, 15.0에서 ppr: 'incremental' 옵션이 추가된 PPR(Partial Prerendering)은 하나의 페이지에서 정적 부분과 동적 부분을 분리하여 렌더링합니다. 정적 셸은 빌드 타임에 생성되어 CDN에서 즉시 제공되고, 동적 부분은 스트리밍으로 채워집니다. 주의: PPR은 아직 experimental 기능이며 프로덕션 사용은 공식적으로 권장되지 않습니다.

// next.config.ts
const nextConfig = {
  experimental: {
    ppr: true,
  },
};

export default nextConfig;
// app/product/[id]/page.tsx
import { Suspense } from 'react';

// 이 컴포넌트는 정적으로 프리렌더링됨
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const product = await fetchProduct(id);
  
  return (
    <main>
      {/* 정적 셸: 빌드 타임에 생성 */}
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      
      {/* 동적 부분: 요청 시 스트리밍 */}
      <Suspense fallback={<PriceSkeleton />}>
        <DynamicPrice productId={id} />
      </Suspense>
      
      <Suspense fallback={<ReviewsSkeleton />}>
        <DynamicReviews productId={id} />
      </Suspense>
    </main>
  );
}

SEO 관점에서 PPR의 이점은 명확합니다. 정적 셸에 SEO 핵심 콘텐츠(제목, 설명, 메타데이터)가 포함되므로 크롤러가 즉시 접근할 수 있고, 동적 부분(가격, 재고, 리뷰)은 스트리밍으로 보완됩니다.

  • Next.js 15+ 사용 시 PPR 적용 가능 여부 검토
  • PPR 적용 페이지에서 SEO 핵심 콘텐츠가 정적 셸에 포함되는지
  • Suspense boundary가 정적/동적 콘텐츠를 올바르게 분리하는지

6. 사이트맵과 robots.txt 점검

체크 6-1: App Router의 사이트맵 자동 생성

App Router에서는 app/sitemap.ts를 통해 동적으로 사이트맵을 생성할 수 있습니다.

// app/sitemap.ts
import type { MetadataRoute } from 'next';

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await fetchAllPosts();

  const blogUrls = posts.map((post) => ({
    url: `https://yoursite.com/blog/${post.slug}`,
    lastModified: post.updatedAt,
    changeFrequency: 'weekly' as const,
    priority: 0.7,
  }));

  return [
    {
      url: 'https://yoursite.com',
      lastModified: new Date(),
      changeFrequency: 'daily',
      priority: 1,
    },
    ...blogUrls,
  ];
}
  • app/sitemap.ts 파일이 존재하고 모든 중요 페이지를 포함하는지
  • noindex 페이지가 사이트맵에서 제외되었는지
  • curl https://yoursite.com/sitemap.xml로 올바른 XML 출력 확인
  • Google Search Console과 네이버 서치어드바이저에 사이트맵 제출 완료

체크 6-2: robots.txt 설정

// app/robots.ts
import type { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: ['/api/', '/admin/'],
    },
    sitemap: 'https://yoursite.com/sitemap.xml',
  };
}
  • app/robots.ts 파일에서 중요 페이지가 차단되지 않는지
  • _next/static/ 경로가 차단되지 않는지 (CSS/JS 크롤링 필요)
  • 사이트맵 URL이 robots.txt에 명시되어 있는지

7. 네이버 전용 점검 항목

네이버 Yeti는 JavaScript 렌더링을 지원하지만 효율이 낮아, 네이버 공식 가이드에서 SSR을 권장합니다.

출처: 네이버 서치어드바이저 - JavaScript 페이지 가이드

"CSR이라고 해서 크롤링이 안 되는 것은 아니지만, 크롤링 효율 면에서 SSR이 낫기 때문에 주요 콘텐츠는 SSR로 렌더링할 것"을 권장

네이버 체크리스트

  • 주요 콘텐츠가 초기 HTML에 포함되는지 (curl로 확인)
  • URL에 # 프래그먼트(해시 라우팅)를 사용하지 않는지
  • 리다이렉트가 HTTP 301/308/302/307로 처리되는지 (HTTP 리다이렉트가 가장 안정적, JS 리다이렉트는 비권장)
  • 네이버 서치어드바이저에 사이트 등록 완료
  • 네이버 서치어드바이저에 사이트맵 제출 완료
  • robots.txt에서 Yeti 크롤러가 차단되지 않는지

Next.js App Router에서 네이버 리다이렉트 처리

// next.config.ts — HTTP 리다이렉트 (네이버가 인식 가능)
const nextConfig = {
  async redirects() {
    return [
      {
        source: '/old-page',
        destination: '/new-page',
        permanent: true, // 308 리다이렉트 (영구 이전)
      },
    ];
  },
};

export default nextConfig;
// 서버 컴포넌트에서의 리다이렉트 (네이버가 인식 가능)
import { redirect } from 'next/navigation';

export default async function OldPage() {
  redirect('/new-page'); // HTTP 리다이렉트로 처리됨
}

클라이언트 측 router.push()는 JS 리다이렉트이므로 네이버가 인식하지 못합니다. SEO가 중요한 리다이렉트는 반드시 next.config.tsredirects 또는 서버 컴포넌트의 redirect()를 사용하세요.


8. 종합 체크리스트 요약

영역점검 항목우선순위
메타데이터generateMetadata fetch 실패 시 fallback높음
상태 코드존재하지 않는 URL에서 notFound() 반환높음
렌더링searchParams로 인한 의도치 않은 동적 전환높음
하이드레이션서버/클라이언트 렌더링 불일치중간
코드 스플리팅핵심 콘텐츠에 ssr: false 사용 여부높음
canonical모든 페이지에 canonical URL 설정중간
사이트맵app/sitemap.ts 구성 및 제출높음
네이버초기 HTML 콘텐츠 포함, HTTP 리다이렉트높음
PPR정적 셸에 SEO 핵심 콘텐츠 포함중간
번들불필요한 'use client' 디렉티브 제거낮음

빠른 진단 명령어

실제 사이트에서 바로 확인할 수 있는 명령어입니다.

# 1. 초기 HTML에 핵심 콘텐츠가 포함되는지 확인
curl -s https://yoursite.com/ | grep -o '<h1>.*</h1>'

# 2. 404 페이지의 상태 코드 확인
curl -s -o /dev/null -w "%{http_code}" https://yoursite.com/존재하지않는경로

# 3. 리다이렉트 체인 확인
curl -sIL https://yoursite.com/old-page 2>&1 | grep -E 'HTTP/|location:'

# 4. 빌드 시 라우트별 렌더링 방식 확인
npm run build 2>&1 | grep -E '○|●|λ|ƒ'

# 5. robots.txt 확인
curl https://yoursite.com/robots.txt

# 6. 사이트맵 확인
curl -s https://yoursite.com/sitemap.xml | head -20

자주 묻는 질문

Q1: Client Components('use client')를 사용하면 SEO에 불리한가요?

아닙니다. Client Components도 서버에서 프리렌더링되어 초기 HTML에 콘텐츠가 포함됩니다. 다만 useEffect 안에서만 렌더링되는 콘텐츠는 초기 HTML에 포함되지 않으므로, SEO에 중요한 텍스트는 useEffect 밖에서 렌더링해야 합니다.

Q2: RSC 스트리밍 콘텐츠를 Google이 제대로 읽나요?

Vercel/MERJ 연구에서 200 상태 코드를 반환하는 RSC 스트리밍 콘텐츠가 100% 렌더링 완료된 것으로 확인되었습니다. 다만 이 데이터는 nextjs.org라는 고권위 사이트 기준이므로, 중소 사이트에서는 렌더링 지연이 더 길 수 있습니다.

Q3: searchParams를 써야 하는 검색 페이지는 어떻게 하나요?

검색 결과 페이지처럼 searchParams가 필수인 경우, 동적 렌더링은 불가피합니다. 다만 카테고리 목록처럼 정적으로 생성할 수 있는 페이지까지 searchParams에 의존하지 않도록 라우트 설계를 분리하세요. 예를 들어 /category/shoes는 정적으로, /search?q=shoes는 동적으로 처리합니다.

Q4: 하이드레이션 불일치가 SEO에 직접적인 영향을 주나요?

하이드레이션 불일치 자체가 검색 순위에 직접 영향을 주지는 않습니다. 하지만 DOM 재구축으로 인한 레이아웃 시프트는 CLS 점수를 악화시키고, 이중 렌더링은 INP 저하로 이어집니다. 두 지표 모두 Core Web Vitals에 포함되어 검색 순위에 영향을 줍니다.

Q5: PPR은 아직 실험적 기능인데, 프로덕션에 사용해도 되나요?

Next.js 15 기준으로 PPR은 실험적(experimental) 기능이지만, Vercel 배포 환경에서는 안정적으로 동작합니다. 셀프 호스팅 환경에서는 스트리밍 인프라 설정이 필요할 수 있습니다. 기존 SSR/SSG로 충분한 사이트라면 급하게 전환할 필요는 없지만, 정적 콘텐츠와 동적 콘텐츠가 혼재하는 이커머스 등의 사이트에서는 검토할 가치가 있습니다.

Q6: 네이버에서 Next.js App Router 사이트가 색인이 안 됩니다. 어떻게 하나요?

먼저 curl로 초기 HTML을 확인하세요. App Router의 Server Components는 기본적으로 서버 렌더링되므로 초기 HTML에 콘텐츠가 포함되어야 합니다. 만약 포함되어 있다면, 네이버 서치어드바이저 등록 여부, 사이트맵 제출 여부, robots.txt에서 Yeti 차단 여부를 순서대로 확인하세요. # 해시 라우팅이나 JS 리다이렉트가 있다면 네이버가 해당 URL을 처리하지 못합니다.


마무리

Next.js App Router는 SEO 관점에서 좋은 출발점을 제공합니다. Server Components가 기본값이고, generateMetadata API로 메타데이터를 체계적으로 관리할 수 있으며, sitemap.tsrobots.ts로 크롤링 설정을 코드로 관리할 수 있습니다.

하지만 이 체크리스트에서 다룬 것처럼, 프레임워크가 제공하는 기능과 실제 SEO 결과 사이에는 개발자가 챙겨야 할 간극이 있습니다. generateMetadata fetch 실패, searchParams로 인한 동적 전환, 하이드레이션 불일치, 소프트 404 — 이 문제들은 모두 코드 리뷰와 빌드 로그 확인으로 사전에 발견할 수 있습니다.

지금 npm run build를 실행해서 의도치 않은 λ(동적) 라우트가 없는지 확인해 보세요. 그것이 가장 빠른 첫 번째 진단입니다.

Next.js SEO 진단이 필요하시면 XEO 무료 진단을 신청하세요.

검색 최적화가 필요하신가요?

무료 상담을 통해 비즈니스에 맞는 최적화 전략을 확인하세요.