title: Next.js에서 Draft Mode(초안 모드)로 콘텐츠 미리 보는 방법
description: Next.js는 정적 페이지와 동적 페이지를 전환할 수 있는 초안 모드(Draft Mode)를 제공합니다. App Router에서 이 기능이 어떻게 동작하는지 여기서 배울 수 있어요.
url: "https://nextjs.org/docs/app/guides/draft-mode"
version: 16.1.6
lastUpdated: 2026-02-27
prerequisites:
Draft Mode(초안 모드)를 사용하면 Next.js 애플리케이션에서 Headless CMS의 초안(Draft) 콘텐츠를 미리 볼 수 있어요. 이 기능은 빌드 시점에 생성되는 정적(Static) 페이지에 아주 유용하답니다. 전체 사이트를 다시 빌드할 필요 없이, 동적 렌더링(Dynamic rendering)으로 전환해서 초안의 변경 사항을 바로바로 확인할 수 있거든요.
이 페이지에서는 Draft Mode를 활성화하고 사용하는 방법을 단계별로 차근차근 알아볼 거예요.
💡 강사의 보충 설명 & 팁:
"Headless CMS"란 WordPress, Contentful, Sanity처럼 화면(View) 없이 콘텐츠 관리용 백엔드만 제공하는 시스템을 말해요.
평소 정적 생성(SSG)을 사용하면 블로그 글을 하나 수정할 때마다 전체 사이트를 다시 빌드(Rebuild)해야 해서 시간이 꽤 걸리죠? 기획자나 마케터가 오타 하나 고치고 결과를 확인하려고 몇 분씩 기다려야 한다면 엄청 답답할 거예요.
바로 이럴 때 Draft Mode를 쓰면, 개발자 도구의 쿠키를 이용해 일시적으로 페이지를 실시간(SSR)으로 그려주게 됩니다. 실무에서 기획자나 에디터분들에게 아주 사랑받는 기능이랍니다!
먼저 Route Handler를 하나 만들어주세요. 파일 이름은 자유롭게 지어도 되지만, 예를 들어 app/api/draft/route.ts처럼 만들면 깔끔하겠죠?
export async function GET(request: Request) {
return new Response('')
}
export async function GET() {
return new Response('')
}
그다음, next/headers에서 draftMode 함수를 불러오고 enable() 메서드를 호출해 주세요.
import { draftMode } from 'next/headers'
export async function GET(request: Request) {
const draft = await draftMode()
draft.enable()
return new Response('Draft mode is enabled')
}
import { draftMode } from 'next/headers'
export async function GET(request) {
const draft = await draftMode()
draft.enable()
return new Response('Draft mode is enabled')
}
이렇게 하면 브라우저에 draft mode를 활성화하는 쿠키(cookie)가 설정됩니다. 이후에 이 쿠키를 포함해서 요청을 보내면 draft mode가 발동되어서, 정적으로 생성되던 페이지들의 동작 방식이 동적으로 바뀌게 돼요.
브라우저에서 직접 /api/draft 주소로 접속한 뒤 개발자 도구(F12)의 Network나 Application 탭을 확인해 보면 수동으로 테스트해 볼 수 있어요. 응답 헤더에 __prerender_bypass라는 이름의 쿠키가 Set-Cookie로 설정된 걸 꼭 확인해 보세요!
💡 강사의 실무 팁:
이 기능이 쿠키 기반이라는 점이 매우 중요해요! 즉,/api/draft에 접속해서 쿠키를 받은 내 브라우저에서만 수정 중인 초안이 보인다는 뜻입니다. 실제 운영 서버에 접속하는 일반 사용자들의 브라우저에는 이 쿠키가 없기 때문에 원래의 정적 페이지(배포된 버전)를 그대로 보게 됩니다. 아주 안전하게 테스트할 수 있죠.
참고: 이 단계들은 여러분이 사용하는 Headless CMS가 커스텀 초안 URL(custom draft URLs) 설정을 지원한다고 가정하고 설명해요. 만약 지원하지 않더라도 이 방법을 써서 초안 URL을 안전하게 보호할 수 있지만, 초안 URL을 직접 만들고 접근해야 할 수도 있어요. 구체적인 방법은 사용 중인 Headless CMS 종류마다 조금씩 다를 수 있답니다.
Headless CMS에서 아까 만든 Route Handler로 안전하게 접근하려면 다음 순서를 따라주세요:
app/api/draft/route.ts에 만들었다고 가정할게요). 예를 들면 이런 형태가 됩니다:https://<your-site>/api/draft?secret=<token>&slug=<path>
<your-site>: 실제 배포된 도메인 주소를 넣으세요.<token>: 1번에서 만든 시크릿 토큰으로 바꿔주세요.<path>: 미리 보고 싶은 페이지의 경로예요. 만약/posts/one을 보고 싶다면,&slug=/posts/one이라고 쓰면 됩니다.CMS에 따라 초안 URL에 변수를 넣을 수 있는 기능이 있을 거예요. 예를 들어
&slug=/posts/{entry.fields.slug}처럼 설정하면 CMS의 데이터에 따라<path>가 동적으로 설정되게 할 수 있죠.
slug 파라미터가 잘 들어왔는지 검사합니다 (둘 중 하나라도 틀리면 요청을 실패 처리해야 해요). 검증이 끝났다면 draftMode.enable()을 호출해서 쿠키를 설정하고, slug로 지정된 경로로 브라우저를 리다이렉트 시켜줍니다:import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request: Request) {
// URL에서 쿼리 스트링 파라미터를 읽어옵니다.
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
// secret과 next 파라미터를 검증합니다.
// 이 secret은 이 Route Handler와 CMS만 알고 있어야 해요!
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
// Headless CMS에서 데이터를 가져와서 전달받은 `slug`가 실제로 존재하는 글인지 확인합니다.
// getPostBySlug 함수는 CMS에서 데이터를 가져오는 여러분만의 로직으로 구현하시면 돼요.
const post = await getPostBySlug(slug)
// 만약 해당 slug의 글이 없다면 초안 모드가 켜지는 걸 막아야겠죠?
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
// 검증이 끝났으니 쿠키를 설정해서 Draft Mode를 켭니다.
const draft = await draftMode()
draft.enable()
// CMS에서 방금 가져온 포스트의 경로로 리다이렉트합니다.
// ❗️주의: searchParams.slug로 바로 리다이렉트하면 안 돼요! 오픈 리다이렉트(Open Redirect) 취약점이 발생할 수 있거든요.
redirect(post.slug)
}
import { draftMode } from 'next/headers'
import { redirect } from 'next/navigation'
export async function GET(request) {
// URL에서 쿼리 스트링 파라미터를 읽어옵니다.
const { searchParams } = new URL(request.url)
const secret = searchParams.get('secret')
const slug = searchParams.get('slug')
// secret과 next 파라미터를 검증합니다.
// 이 secret은 이 Route Handler와 CMS만 알고 있어야 해요!
if (secret !== 'MY_SECRET_TOKEN' || !slug) {
return new Response('Invalid token', { status: 401 })
}
// Headless CMS에서 데이터를 가져와서 전달받은 `slug`가 실제로 존재하는 글인지 확인합니다.
// getPostBySlug 함수는 CMS에서 데이터를 가져오는 여러분만의 로직으로 구현하시면 돼요.
const post = await getPostBySlug(slug)
// 만약 해당 slug의 글이 없다면 초안 모드가 켜지는 걸 막아야겠죠?
if (!post) {
return new Response('Invalid slug', { status: 401 })
}
// 검증이 끝났으니 쿠키를 설정해서 Draft Mode를 켭니다.
const draft = await draftMode()
draft.enable()
// CMS에서 방금 가져온 포스트의 경로로 리다이렉트합니다.
// ❗️주의: searchParams.slug로 바로 리다이렉트하면 안 돼요! 오픈 리다이렉트(Open Redirect) 취약점이 발생할 수 있거든요.
redirect(post.slug)
}
💡 강사의 보안 팁 (오픈 리다이렉트 방지):
코드 주석에도 나와 있지만, 유저가 입력한slug파라미터를 믿고 그대로redirect(slug)해버리는 건 보안상 아주 위험해요. 악의적인 해커가?slug=https://phishing-site.com같이 피싱 사이트 주소를 넣으면 우리 서버가 그쪽으로 보내버리게 되거든요!
그래서 위 코드처럼 CMS에 한 번 물어봐서 진짜 있는 글인지(getPostBySlug) 확인한 뒤, 데이터베이스나 CMS에서 내려준 안전한 경로(post.slug)로 리다이렉트하는 것이 핵심입니다!
성공적으로 처리가 완료되면, 브라우저에는 draft mode 쿠키가 심어진 채로 여러분이 보고 싶어 했던 초안 페이지 경로로 이동하게 됩니다.
자, 이제 마지막 단계예요. 페이지 컴포넌트를 수정해서 draftMode().isEnabled의 값을 확인하도록 만들어야 합니다.
쿠키가 설정된 상태로 페이지를 요청하면, 빌드 시점(Build time)이 아니라 요청 시점(Request time)에 실시간으로 데이터를 가져오게 돼요.
그리고 이때 isEnabled 값은 true가 됩니다.
// 데이터를 가져오는 페이지
import { draftMode } from 'next/headers'
async function getData() {
// 현재 초안 모드인지 확인해요
const { isEnabled } = await draftMode()
// 초안 모드면 초안 데이터를 주는 API로, 아니면 실제 운영 API로 요청합니다.
const url = isEnabled
? '[https://draft.example.com](https://draft.example.com)'
: '[https://production.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>
)
}
// 데이터를 가져오는 페이지
import { draftMode } from 'next/headers'
async function getData() {
// 현재 초안 모드인지 확인해요
const { isEnabled } = await draftMode()
// 초안 모드면 초안 데이터를 주는 API로, 아니면 실제 운영 API로 요청합니다.
const url = isEnabled
? '[https://draft.example.com](https://draft.example.com)'
: '[https://production.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>
)
}
이제 모든 세팅이 끝났습니다! Headless CMS에서 미리보기 버튼을 누르거나, 브라우저 주소창에 수동으로 secret과 slug를 포함한 URL을 쳐서 접속해 보세요. 작성 중인 초안 콘텐츠가 보일 거예요. 앞으로 글을 발행(Publish)하지 않고 임시저장만 한 상태에서도 수정 사항을 실시간으로 확인할 수 있게 되었습니다! 👏
Draft Mode를 더 자세히 다루고 싶다면 아래 API 문서를 참고해 보세요.
draftMode 함수에 대한 상세한 API 레퍼런스입니다.모든 문서의 의미론적 개요(Semantic overview)를 보려면 https://nextjs.org/docs/sitemap.md를 확인하세요.
사용 가능한 전체 문서의 인덱스(Index)를 보려면 https://nextjs.org/docs/llms.txt를 확인하세요.