무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
Next.js 16 ISR generateStaticParams 필수 — generative 키 비주얼
museck 만들기
2026. 2. 23.

Next.js 16 ISR 미스터리: generateStaticParams가 없으면 ISR도 없다

Google Search Console에서 "색인 생성 안 됨" 경고가 쏟아졌다. 블로그 포스트 60개 넘게 발행했는데, 검색엔진이 절반을 인식하지 못하고 있었다.

모든 동적 [slug] 페이지에 revalidate = 60을 설정해 두었으니 ISR이 잘 동작하고 있을 거라 생각했다. 그런데 실제로는 전부 fully dynamic이었다. 매 요청마다 서버에서 렌더링하고, 캐시도 없고, 응답 시간도 느렸다. 왜?

8개 커밋에 걸친 디버깅

ISR이 안 되는 원인 후보는 여러 가지였다. Next.js 16 canary의 ISR 동작 자체가 바뀌었을 수도 있고, 페이지 내부의 특정 API 호출이 dynamic을 강제할 수도 있었다. 하나씩 가설을 세우고 검증했다.

가설 1: draftMode()가 dynamic을 강제한다

가장 먼저 의심한 건 draftMode()였다. PayloadCMS의 미리보기 기능을 위해 각 상세 페이지에서 호출하고 있었는데, 이 함수는 쿠키를 읽기 때문에 Next.js가 페이지를 dynamic으로 분류한다. 맞았다 — 제거하니 더 이상 강제 dynamic이 아니었다. 하지만 ISR은 여전히 안 됐다.

// Before — draftMode()가 dynamic을 강제
export default async function Page({ params }) {
  const { isEnabled } = await draftMode()
  // ...
}

// After — draftMode 제거
export default async function Page({ params }) {
  const { slug } = await params
  // 직접 getPayload 호출
}

가설 2: 'use cache' 함수가 ISR과 충돌한다

데이터 조회 함수에 'use cache' 지시어를 쓰고 있었다. Next.js의 데이터 캐시 레이어와 ISR의 페이지 캐시 레이어가 겹치면서 빌드 타임의 빈 결과가 캐시에 고착되는 현상이 발생했다. 직접 getPayload를 호출하는 방식으로 교체했다.

// ISR 페이지에서는 직접 조회
async function getPost(slug: string) {
  if (process.env.NEXT_PHASE === 'phase-production-build') return null
  const payload = await getPayload({ config })
  const result = await payload.find({
    collection: 'posts',
    where: { slug: { equals: slug }, _status: { equals: 'published' } },
    limit: 1,
  })
  return result.docs[0] ?? null
}

결정적 발견: generateStaticParams

draftMode()를 제거하고 'use cache'도 걷어냈는데 여전히 ISR이 안 됐다. 실험 커밋을 만들어서 가설을 하나씩 검증했다.

b718e6f — 가설: generateMetadata가 문제? → 제거해도 변화 없음 ❌
9de6149 — 가설: generateStaticParams가 필요? → 빈 배열 추가 → ISR 작동! ✅

빈 배열을 반환하는 generateStaticParams를 추가하자 ISR이 즉시 작동했다. Next.js 16 canary에서 [slug] 동적 라우트는 이 함수가 있어야만 ISR 대상으로 인식된다. revalidate 값만으로는 부족하다.

// 이것만 추가하면 ISR이 활성화된다
export async function generateStaticParams() {
  return []
}

export const revalidate = 60

이건 Next.js 공식 문서에 명시되어 있지 않다. 문서에는 revalidate만 설정하면 ISR이 동작한다고 되어 있다. 하지만 canary 버전에서는 동작이 다르다. generateStaticParams의 존재 자체가 "이 라우트는 정적 생성 대상"이라는 신호로 작용하는 것 같다. 빈 배열을 반환하더라도 빌드 타임에 생성할 페이지는 없지만, 런타임에 ISR로 생성하겠다는 의도가 전달된다.

ISR 활성화 체크리스트

이 디버깅을 통해 Next.js 16 canary에서 ISR을 제대로 동작시키기 위한 조건 목록을 정리했다.

  • revalidate export — 재검증 주기 설정
  • generateStaticParams — [slug] 라우트에 필수 (빈 배열이라도)
  • draftMode() 호출 제거 — 쿠키 접근이 dynamic을 강제
  • 'use cache' 함수 호출 제거 — 캐시 레이어 충돌 방지
  • NEXT_PHASE 가드 — 빌드 타임에 DB 없이 빈 결과 반환
  • searchParams 사용 금지 — 있으면 자동으로 fully dynamic

이 중 하나라도 위반하면 ISR이 작동하지 않거나 예측 불가능한 캐싱 동작이 발생한다.

실험 커밋의 가치

이번 디버깅에서 가장 효과적이었던 건 가설을 커밋으로 남기는 방식이었다. 코드를 변경하고 배포해서 동작을 확인한 뒤, 커밋 메시지에 가설과 결과를 기록했다. "generateMetadata 제거 → 변화 없음", "generateStaticParams 추가 → ISR 작동" 같은 형태다.

이렇게 하면 나중에 같은 문제를 만났을 때 git log에서 답을 찾을 수 있다. 블로그에 글을 쓰는 것과 비슷하지만, 커밋 히스토리는 코드와 함께 살아남기 때문에 접근성이 더 좋다. "그때 왜 이렇게 바꿨지?"라는 질문에 커밋 메시지가 바로 답한다.

결과적으로 모든 [slug] 라우트에 빈 generateStaticParams를 추가하고, draftMode()와 'use cache'를 걷어내자 ISR이 정상 작동했다. Google Search Console의 색인 경고도 점차 해소되기 시작했다. canary 버전을 쓴다면 문서를 믿지 말고 직접 검증하자.

자주 묻는 질문

generateStaticParams에서 빈 배열을 반환해도 괜찮은가?

괜찮다. 빈 배열을 반환하면 빌드 타임에 정적 생성할 페이지는 없지만, Next.js에게 이 라우트가 ISR 대상이라는 신호를 보낸다. 실제 페이지는 런타임에 요청이 들어올 때 생성되고 revalidate 주기로 캐시된다.

draftMode를 완전히 제거하면 미리보기는 어떻게 하나?

PayloadCMS의 Live Preview 기능을 대신 사용하거나, /api/preview 같은 별도 API 라우트에서 draftMode를 활성화하고 리다이렉트하는 방식으로 분리한다. ISR 페이지 자체에서 draftMode()를 호출하지 않는 게 핵심이다.

'use cache'와 ISR을 같은 페이지에서 쓸 수 없는 이유는?

'use cache'는 Next.js의 데이터 캐시 레이어에서 동작하고, ISR은 페이지 레벨 캐시에서 동작한다. 두 캐시 레이어가 겹치면 revalidation 타이밍이 꼬여서 빌드 타임의 빈 결과가 캐시에 고착될 수 있다. ISR 페이지에서는 직접 getPayload를 호출하는 게 안전하다.

자주 묻는 질문

generateStaticParams에서 빈 배열을 반환해도 괜찮은가?
괜찮다. 빈 배열을 반환하면 빌드 타임에 정적 생성할 페이지는 없지만, Next.js에게 이 라우트가 ISR 대상이라는 신호를 보낸다. 실제 페이지는 런타임에 요청이 들어올 때 생성되고 revalidate 주기로 캐시된다.
draftMode를 완전히 제거하면 미리보기는 어떻게 하나?
PayloadCMS의 Live Preview 기능을 대신 사용하거나, /api/preview 같은 별도 API 라우트에서 draftMode를 활성화하고 리다이렉트하는 방식으로 분리한다. ISR 페이지 자체에서 draftMode()를 호출하지 않는 게 핵심이다.
use cache와 ISR을 같은 페이지에서 쓸 수 없는 이유는?
use cache는 Next.js의 데이터 캐시 레이어에서 동작하고, ISR은 페이지 레벨 캐시에서 동작한다. 두 캐시 레이어가 겹치면 revalidation 타이밍이 꼬여서 빌드 타임의 빈 결과가 캐시에 고착될 수 있다. ISR 페이지에서는 직접 getPayload를 호출하는 게 안전하다.
museck 만들기(20/22)
Prev

기술 블로그 품질 강화: Mermaid 다이어그램, FAQ 스키마, OG 이미지, Favicon, CDN 캐시

Next

Umami + Next.js Rewrites: 프라이버시 웹 분석 프록시 구축기