Next.js Draft Mode 가이드 정리

1. Draft Mode 개요

Draft Mode는 Headless CMS에서 아직 발행되지 않은 콘텐츠(초안)를 Next.js 애플리케이션에서 미리보기 할 수 있도록 도와주는 기능입니다.

  • 평소에는 정적 렌더링(SSG)을 사용하더라도, 특정 요청에 한해서만 동적 렌더링으로 전환하여 초안 데이터를 가져올 수 있습니다.
  • Draft Mode가 활성화되면 Next.js가 **특정 쿠키(__prerender_bypass)**를 기반으로 해당 요청을 빌드 타임이 아닌 요청 시점에 렌더링합니다.

구성은 크게 세 단계입니다.

  1. Draft Mode를 활성화하는 Route Handler 만들기
  2. Headless CMS에서 이 Route Handler를 안전하게 호출하도록 설정하기
  3. 페이지에서 draftMode().isEnabled 값을 확인해 초안/발행본을 나누어 가져오기

2. Step 1: Route Handler 만들기

먼저 Draft Mode를 켜는 API 엔드포인트를 생성합니다. 이름은 자유지만 예시로 app/api/draft/route.ts를 사용합니다.

// app/api/draft/route.ts
export async function GET(request: Request) {
  return new Response('')
}

이제 Next.js에서 제공하는 draftMode 함수를 import하고, enable()을 호출합니다.

// app/api/draft/route.ts
import { draftMode } from 'next/headers'

export async function GET(request: Request) {
  const draft = await draftMode()
  draft.enable()
  return new Response('Draft mode is enabled')
}

이 핸들러를 호출하면 Next.js가 응답에 쿠키를 추가해 Draft Mode를 활성화합니다.

  • 브라우저에서 /api/draft로 직접 접속하고, 개발자 도구의 Network 탭에서 응답 헤더를 확인하면
    • Set-Cookie 헤더에 __prerender_bypass 쿠키가 설정되어 있는 것을 볼 수 있습니다.
  • 이 쿠키가 포함된 이후 요청들은 Draft Mode가 활성화된 상태가 됩니다.

3. Step 2: Headless CMS에서 Route Handler 접근

대부분의 Headless CMS는 커스텀 미리보기 URL을 설정하는 기능을 제공합니다. 이를 활용하여 Draft Mode를 보안적으로 활성화합니다.

  1. 비밀 토큰(secret) 생성
    • 임의의 토큰 문자열을 하나 만들고, Next.js 앱과 CMS만 알고 있도록 관리합니다.
  2. CMS에 미리보기 URL 등록 (예시)
https://<your-site>/api/draft?secret=<token>&slug=<path>
  • <your-site>: 배포된 사이트 도메인
  • <token>: 방금 생성한 secret 토큰
  • <path>: 미리 보고 싶은 페이지 경로
    • 예: 초안 페이지가 /posts/one이라면 &slug=/posts/one
    • CMS에서 slug 변수를 활용해 동적으로 구성할 수도 있습니다.
      • 예: &slug=/posts/{entry.fields.slug}
  1. Route Handler 내부에서 secretslug를 검증하고, 유효할 때만 Draft Mode를 활성화한 뒤 해당 페이지로 리다이렉트합니다.
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'

export async function GET(request: Request) {
  // 쿼리 파라미터 파싱
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')

  // secret 및 slug 검증
  // 이 secret은 이 Route Handler와 CMS만 알고 있어야 합니다.
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }

  // CMS에 실제로 해당 slug를 가진 콘텐츠가 존재하는지 확인
  const post = await getPostBySlug(slug)

  // 잘못된 slug라면 Draft Mode를 켜지 않음
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }

  // Draft Mode 활성화 (쿠키 설정)
  const draft = await draftMode()
  draft.enable()

  // 안전한 slug로 리다이렉트
  // 사용자가 전달한 slug 문자열을 그대로 쓰지 않고,
  // CMS에서 검증한 post.slug를 사용해 Open Redirect 취약성을 방지합니다.
  redirect(post.slug)
}

위 코드가 성공적으로 실행되면:

  • 브라우저는 post.slug 경로로 리다이렉트됩니다.
  • 요청에는 Draft Mode 쿠키가 포함되어 있으므로, 해당 페이지는 초안 콘텐츠를 기준으로 렌더링됩니다.

4. Step 3: Draft 콘텐츠 미리보기

이제 페이지 코드에서 Draft Mode 활성 여부에 따라 다른 데이터 소스를 선택해야 합니다.

// app/page.tsx
// 데이터를 가져오는 페이지 예시
import { draftMode } from 'next/headers'

async function getData() {
  const { isEnabled } = await draftMode()

  const url = isEnabled
    ? 'https://draft.example.com'       // 초안 API 엔드포인트
    : 'https://production.example.com'  // 발행본 API 엔드포인트

  const res = await fetch(url)
  return res.json()
}

export default async function Page() {
  const { title, desc } = await getData()

  return (
    <main>
      <h1>{title}</h1>
      <p>{desc}</p>
    </main>
  )
}

동작 흐름:

  1. CMS에서 설정한 미리보기 버튼을 클릭하면 /api/draft?secret=...&slug=...가 호출됩니다.
  2. Route Handler가 secret·slug를 검증하고, Draft Mode 쿠키를 설정합니다.
  3. 브라우저가 slug 경로로 리다이렉트됩니다.
  4. 해당 페이지는 draftMode().isEnabled 값을 확인해, 초안용 API 엔드포인트에서 데이터를 가져옵니다.
  5. CMS에서 초안을 수정·저장하면, 발행하지 않아도 새로고침만으로 변경된 초안 내용을 확인할 수 있습니다.

5. 정리 및 활용 팁

  • Draft Mode는 전체 사이트를 동적으로 만드는 것이 아니라, 특정 요청에서만 SSG → 동적 렌더링으로 전환하는 기능입니다.
  • 실제 서비스에서는 다음과 같이 활용할 수 있습니다.
    • 운영 사이트는 그대로 SSG/ISR로 빠르게 서비스
    • 마케터·에디터는 CMS에서 초안을 저장 후, 미리보기 URL로 실제 레이아웃에서 검수
    • 초안이 완성되면 CMS에서 발행(Publish)하여 정식 반영
  • 보안을 위해:
    • secret 토큰은 환경 변수로 관리하는 것이 좋습니다.
    • slug 검증 시 CMS에서 실제 존재 여부를 체크해야 합니다.
    • 임의의 redirect 경로를 허용하지 않도록 반드시 검증된 slug만 사용합니다.