Next.js 16 + Sanity 도입기3분 읽기 · 0%
도입기

Next.js 16 + Sanity 도입기

Next.js 16 + Sanity
도입기

Next.js 16 + Sanity v5로 구축한 풀스택 블로그 아키텍처 정리

도입 배경

컴포넌트팀 공식 사이트는 Next.js 16 App Router 기반으로 운영 중이었지만, 마케팅·기술 인사이트를 정기적으로 발행할 콘텐츠 채널이 부재했습니다.

이를 해결하기 위해 다음 4가지 요구사항을 만족하는 CMS가 필요했습니다.

  1. 비개발자가 직접 발행할 수 있는 Studio UI
  2. 예약 발행과 Draft Mode 기반 미리보기 워크플로
  3. 발행 즉시 SEO·sitemap에 반영되는 구조
  4. 한국어 슬러그 및 한글 콘텐츠에 친화적인 시스템

여러 옵션을 검토한 결과 Sanity v5 + next-sanity v12 조합이 가장 적합했습니다. Headless 아키텍처 덕분에 기존 Next.js 라우팅에 자연스럽게 통합되었고, Studio를 같은 도메인(/studio)에 임베드해 별도 호스팅 비용 없이 운영할 수 있었습니다.

---

아키텍처 개요

라우트 구조는 다음과 같이 정리했습니다.

  • app/(site)/ — 기존 마케팅 페이지 + JSON-LD(Organization/FAQPage 등)
  • app/(blog)/blog/page.tsx — 블로그 목록 페이지 (sanityFetch 기반)
  • app/(blog)/blog/[slug]/page.tsx — 블로그 상세 페이지 (generateStaticParams + ISR)
  • app/studio/[[...tool]]/page.tsx — 임베디드 Sanity Studio
  • app/api/revalidate/ — Sanity Webhook 수신 → revalidateTag 호출
  • app/api/draft-mode/ — Sanity Presentation Tool용 미리보기 엔드포인트

핵심 의사결정 3가지

  1. defineLive + sanityFetch 표준 패턴 채택
    • Next.js 캐시 태그를 자동으로 부착해, 이후 ISR·Webhook 기반 무효화 전략을 단순화했습니다.
  2. 라우트 그룹 분리로 JSON-LD 누수 방지
    • 마케팅 사이트의 Organization/FAQPage JSON-LD가 블로그·Studio 라우트로 섞이지 않도록 (site) 그룹으로 격리했습니다.
  3. Webhook 기반 ISR 도입
    • Sanity에서 문서가 발행되면 Webhook이 app/api/revalidate를 호출합니다.
    • 서버에서는 revalidateTag(_type, "max")로 관련 페이지를 즉시 재검증하고, sitemap 역시 동일 태그 전략으로 자동 갱신되도록 설계했습니다.

---

트러블슈팅: Next.js 16 + Turbopack에서 만난 4가지 함정

1. defineEnableDraftMode 모듈 import 충돌

  • next-sanity v12.3.1defineEnableDraftMode 헬퍼가 Next 16의 internal app-router-context.js 경로를 직접 import하면서 MODULE_UNPARSABLE 에러가 발생했습니다.
  • 해결: defineEnableDraftMode 사용을 피하고, validatePreviewUrl을 직접 호출하는 route handler를 구현해 Draft Mode를 수동으로 활성화했습니다.

2. revalidateTag 시그니처 변경

  • Next.js 16부터 revalidateTag(tag, profile) 시그니처가 강제되며, 기존 1-인자 버전은 deprecated 경고를 발생시킵니다.
  • 해결: Webhook 핸들러에서 revalidateTag(_type, "max") 형태로 두 번째 인자에 프로필을 명시해 문제를 해결했습니다.

3. 한글 슬러그 라우트 404 문제

  • /blog/가나다와 같은 경로 접근 시 404가 발생했습니다.
  • 원인: Next 16이 [slug] 파라미터를 URL 인코딩된 상태로 그대로 전달하기 때문에, GROQ 쿼리에서 슬러그가 매칭되지 않았습니다.
  • 해결: 라우트 핸들러에서 decodeURIComponent(rawSlug)를 호출해 디코딩한 값을 GROQ에 전달하도록 수정했습니다.

4. Studio createContext 에러

  • next-sanity/studioNextStudio 컴포넌트가 모듈 평가 시점에 createContext를 호출하면서 server component 환경과 충돌했습니다.
  • 해결: Studio 페이지 컴포넌트(app/studio/[[...tool]]/page.tsx) 상단에 "use client" 디렉티브를 명시해 클라이언트 컴포넌트로 강제했습니다.
  • 이때 metadata/viewport export는 server 컴포넌트에서만 허용되므로, layout.tsx로 분리해 충돌을 피했습니다.
  • #Next.js
  • #Sanity
  • #블로그
  • #CMS