Draft Mode

정적 렌더링은 페이지가 헤드리스 CMS에서 데이터를 가져올 때 유용합니다. 그러나 헤드리스 CMS에서 초안을 작성하고 페이지에서 즉시 초안을 보고 싶을 때는 이상적이지 않습니다. 이러한 경우 Next.js가 이 페이지들을 빌드 타임이 아닌 요청 타임에 렌더링하고, 게시된 콘텐츠 대신 초안 콘텐츠를 가져오도록 하고 싶을 것입니다. Next.js가 이 특정 경우에만 동적 렌더링으로 전환되도록 하고 싶을 것입니다.

Next.js는 이 문제를 해결하는 Draft Mode라는 기능을 제공합니다. 사용 방법은 다음과 같습니다.

Step 1: Create and access the Route Handler

먼저, Route Handler를 생성합니다. 이름은 app/api/draft/route.ts와 같이 임의로 지정할 수 있습니다.

그런 다음, next/headers에서 draftMode를 가져와 enable() 메서드를 호출합니다.

app/api/draft/route.ts
// Draft 모드를 활성화하는 라우트 핸들러
import { draftMode } from 'next/headers'
 
export async function GET(request: Request) {
  draftMode().enable()
  return new Response('Draft mode is enabled')
}
app/api/draft/route.js
// Draft 모드를 활성화하는 라우트 핸들러
import { draftMode } from 'next/headers'
 
export async function GET(request) {
  draftMode().enable()
  return new Response('Draft mode is enabled')
}

이렇게 하면 Draft 모드를 활성화하는 쿠키가 설정됩니다. 이 쿠키를 포함하는 이후의 요청은 Draft Mode를 트리거하여 정적으로 생성된 페이지의 동작을 변경합니다(이후에 더 설명함).

브라우저의 개발자 도구에서 Set-Cookie 응답 헤더와 __prerender_bypass라는 쿠키를 확인하여 /api/draft를 방문하여 수동으로 테스트할 수 있습니다.

Securely accessing it from your Headless CMS

실제로는 헤드리스 CMS에서 이 라우트 핸들러를 보안 방식으로 호출하고 싶을 것입니다. 사용 중인 헤드리스 CMS에 따라 구체적인 단계는 다를 수 있지만, 다음과 같은 일반적인 단계를 따를 수 있습니다.

이 단계는 사용 중인 헤드리스 CMS가 사용자 정의 초안 URL을 설정할 수 있는 기능을 지원한다고 가정합니다. 그렇지 않다면 이 방법을 사용하여 초안 URL을 보호할 수 있지만, 초안 URL을 수동으로 구성하고 접근해야 합니다.

먼저, 원하는 토큰 생성기를 사용하여 시크릿 토큰 문자열을 생성해야 합니다. 이 시크릿은 Next.js 앱과 헤드리스 CMS만 알고 있어야 합니다. 이 시크릿은 CMS에 액세스할 수 없는 사람들이 초안 URL에 접근하는 것을 방지합니다.

두 번째로, 헤드리스 CMS가 사용자 정의 초안 URL을 설정하는 기능을 지원하는 경우, 다음을 초안 URL로 지정합니다. 이는 라우트 핸들러가 app/api/draft/route.ts에 위치한다고 가정합니다.

Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>
  • <your-site>는 배포된 도메인이어야 합니다.
  • <token>은 생성한 시크릿 토큰으로 교체해야 합니다.
  • <path>는 보고자 하는 페이지의 경로여야 합니다. /posts/foo를 보고자 한다면 &slug=/posts/foo를 사용해야 합니다.

사용 중인 헤드리스 CMS는 변수 삽입을 지원할 수 있으며, 따라서 <path>를 CMS의 데이터에 따라 동적으로 설정할 수 있습니다: &slug=/posts/{entry.fields.slug}

마지막으로, 라우트 핸들러에서:

  • 시크릿이 일치하고 slug 매개변수가 존재하는지 확인합니다(없으면 요청이 실패해야 함).
  • draftMode.enable()을 호출하여 쿠키를 설정합니다.
  • 그런 다음 브라우저를 slug로 지정된 경로로 리디렉션합니다.
app/api/draft/route.ts
// 시크릿과 slug를 사용하는 라우트 핸들러
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')
 
  // 시크릿과 다음 매개변수 확인
  // 이 시크릿은 이 라우트 핸들러와 CMS만 알고 있어야 합니다
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }
 
  // 제공된 `slug`가 존재하는지 확인하기 위해 헤드리스 CMS를 가져옵니다
  // getPostBySlug는 헤드리스 CMS에 필요한 가져오기 로직을 구현합니다
  const post = await getPostBySlug(slug)
 
  // `slug`가 존재하지 않으면 Draft 모드가 활성화되지 않도록 합니다
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }
 
  // Draft Mode를 활성화하여 쿠키 설정
  draftMode().enable()
 
  // 가져온 게시물의 경로로 리디렉션
  // 검색 매개변수 slug로 리디렉션하지 않습니다. 이는 열린 리디렉션 취약성으로 이어질 수 있습니다
  redirect(post.slug)
}
app/api/draft/route.js
// 시크릿과 slug를 사용하는 라우트 핸들러
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
 
export async function GET(request) {
  // 쿼리 문자열 매개변수 파싱
  const { searchParams } = new URL(request.url)
  const secret = searchParams.get('secret')
  const slug = searchParams.get('slug')
 
  // 시크릿과 다음 매개변수 확인
  // 이 시크릿은 이 라우트 핸들러와 CMS만 알고 있어야 합니다
  if (secret !== 'MY_SECRET_TOKEN' || !slug) {
    return new Response('Invalid token', { status: 401 })
  }
 
  // 제공된 `slug`가 존재하는지 확인하기 위해 헤드리스 CMS를 가져옵니다
  // getPostBySlug는 헤드리스 CMS에 필요한 가져오기 로직을 구현합니다
  const post = await getPostBySlug(slug)
 
  // `slug`가 존재하지 않으면 Draft 모드가 활성화되지 않도록 합니다
  if (!post) {
    return new Response('Invalid slug', { status: 401 })
  }
 
  // Draft Mode를 활성화하여 쿠키 설정
  draftMode().enable()
 
  // 가져온 게시물의 경로로 리디렉션
  // 검색 매개변수 slug로 리디렉션하지 않습니다. 이는 열린 리디렉션 취약성으로 이어질 수 있습니다
  redirect(post.slug)
}

성공하면 브라우저는 보고자 하는 경로로 리디렉션됩니다.

Step 2: Update page

다음 단계는 페이지에서 draftMode().isEnabled 값을 확인하도록 업데이트하는 것입니다.

쿠키가 설정된 페이지를 요청하면 빌드 타임이 아닌 요청 타임에 데이터가 가져와집니다.

또한, isEnabled 값은 true가 됩니다.

app/page.tsx
// 데이터를 가져오는 페이지
import { draftMode } from 'next/headers'
 
async function getData() {
  const { isEnabled } = draftMode()
 
  const url = isEnabled
    ? 'https://draft.example.com'
    : 'https://production.example.com'
 
  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>
  )
}
app/page.js
// 데이터를 가져오는 페이지
import { draftMode } from 'next/headers'
 
async function getData() {
  const { isEnabled } = draftMode()
 
  const url = isEnabled
    ? 'https://draft.example.com'
    : 'https://production.example.com'
 
  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>
  )
}

이제 끝입니다! 헤

드리스 CMS 또는 수동으로 초안 라우트 핸들러(시크릿과 slug 포함)에 접근하면 초안 콘텐츠를 볼 수 있습니다. 초안을 게시하지 않고 업데이트하더라도 초안을 볼 수 있어야 합니다.

헤드리스 CMS에 이를 초안 URL로 설정하거나 수동으로 접근하면 초안을 볼 수 있습니다.

Terminal
https://<your-site>/api/draft?secret=<token>&slug=<path>

More Details

Clear the Draft Mode cookie

기본적으로 Draft Mode 세션은 브라우저가 닫힐 때 종료됩니다.

Draft Mode 쿠키를 수동으로 지우려면 draftMode().disable()을 호출하는 라우트 핸들러를 생성합니다:

app/api/disable-draft/route.ts
import { draftMode } from 'next/headers'
 
export async function GET(request: Request) {
  draftMode().disable()
  return new Response('Draft mode is disabled')
}
app/api/disable-draft/route.js
import { draftMode } from 'next/headers'
 
export async function GET(request) {
  draftMode().disable()
  return new Response('Draft mode is disabled')
}

그런 다음 /api/disable-draft에 요청을 보내 라우트 핸들러를 호출합니다. next/link를 사용하여 이 경로를 호출하는 경우, 쿠키가 사전 로드 시 실수로 삭제되지 않도록 prefetch={false}를 전달해야 합니다.

Unique per next build

매번 next build를 실행할 때마다 새로운 우회 쿠키 값이 생성됩니다.

이는 우회 쿠키를 추측할 수 없도록 보장합니다.

참고: 로컬에서 HTTP를 통해 Draft Mode를 테스트하려면 브라우저에서 타사 쿠키 및 로컬 스토리지 액세스를 허용해야 합니다.