RSS듀오랩스
Next.js

Next.js ISR과 generateStaticParams

작성자
듀오랩스 대표·9분 읽기

로딩 화면 파일 하나의 위치를 옮겨 push 했는데 Vercel 빌드가 실패했습니다. 페이지의 동작을 바꾸는 코드는 건드리지 않은 커밋이었습니다. 빌드 로그의 오류 줄은 이랬습니다.

[blog] listPosts failed: Query read timeout
Error occurred prerendering page "/category".
Export encountered an error on /category/page: /category, exiting the build.

코드는 멀쩡했습니다. 빌드가 데이터베이스에 접속했고, 그 조회가 시간 초과로 실패했습니다. 같은 커밋으로 다시 배포하자 58초 만에 성공했습니다. 이 블로그의 빌드가 데이터베이스를 타는 이유는 페이지를 미리 만들어 두는 방식, 즉 정적 렌더링과 ISR 설정에 있습니다.

revalidate 를 선언하면 캐시된다는 생각

페이지 파일에 한 줄을 더하면 그 페이지가 캐시된다고 흔히 이해합니다.

export const revalidate = 300;

「이 페이지를 300초 동안 캐시하고, 지나면 새로 만든다」는 뜻으로 읽힙니다. 그렇다면 모든 페이지에 이 줄만 넣으면 되고, 그 페이지가 언제 처음 만들어지는지는 신경 쓸 필요가 없어 보입니다.

이 이해로는 두 가지가 설명되지 않습니다. revalidate 를 선언했는데도 요청마다 새로 렌더링되는 동적 경로가 있다는 것, 그리고 반대로 코드와 상관없는 빌드가 데이터베이스 때문에 실패한다는 것입니다. 페이지가 언제 만들어지는지를 정하는 설정이 따로 있습니다.

이 글은 Cache Components 를 켜지 않은 이전 캐싱 모델 기준입니다. Next.js 16 문서는 두 모델의 가이드를 나눠 두고 있습니다.

페이지가 만들어지는 세 시점

Next.js 의 페이지는 세 시점 중 하나에 만들어집니다.

시점 이름 결과
빌드할 때 정적 렌더링(프리렌더) 빌드 결과물에 HTML 이 들어 있다
첫 요청 때 만들고 보관 ISR 처음 방문한 사람이 만들고, 다음 사람부터 보관본을 받는다
요청마다 동적 렌더링 매번 새로 만든다

revalidate = 300 은 이 중 보관본의 수명을 정합니다. 보관본을 언제 처음 만드는지는 정하지 않습니다.

동적 세그먼트가 없는 경로, 예를 들어 전체 글 목록 / 이나 카테고리 목록 /category 는 빌드할 때 프리렌더됩니다. 페이지가 데이터베이스를 읽는다면 빌드가 데이터베이스를 읽습니다. 처음의 빌드 실패가 이것이었습니다. /category 를 프리렌더하려고 카테고리 목록을 조회하다 시간이 초과됐습니다. 이 블로그의 루트 페이지 주석에는 이 사실이 미리 적혀 있었습니다. 빌드 시점에 데이터베이스가 응답하지 않으면 빌드가 실패하지만, 직전 배포는 그대로 살아 있으므로 안전한 실패라는 내용입니다.

동적 경로의 ISR 에는 generateStaticParams 가 필요하다

/category/[name] 같은 동적 세그먼트는 사정이 다릅니다. 빌드할 때 어떤 name 이 있는지 모르니 미리 만들 수 없습니다. 여기에 revalidate 만 선언하면 ISR 이 될 것 같지만, generateStaticParams 문서에는 조건이 적혀 있습니다.

You must return an empty array from generateStaticParams or utilize export const dynamic = 'force-static' in order to revalidate (ISR) paths at runtime.

실행 중에 경로를 ISR 로 만들려면 generateStaticParams빈 배열을 돌려주거나 dynamic = 'force-static' 을 써야 합니다. 이 블로그의 동적 경로들은 그래서 이렇게 되어 있습니다.

export const revalidate = 300;

// 빌드 때는 아무 카테고리도 렌더하지 않는다(DB 를 타지 않는다).
// 방문한 카테고리만 그때 만들어 캐시한다.
export async function generateStaticParams() {
  return [];
}

빈 배열은 「빌드 때는 하나도 만들지 말고, 요청이 오면 그때 만들어 보관하라」는 뜻입니다. 빌드가 데이터베이스를 타지 않으면서도 방문한 경로는 보관됩니다. 문서는 한 가지를 더 적습니다. ISR 로 다시 만들 때는 generateStaticParams 가 다시 호출되지 않습니다.

반대로 자주 방문하는 경로를 빌드 때 미리 만들어 두고 싶다면 그 목록을 돌려주면 됩니다. 대신 그만큼 빌드가 데이터베이스를 더 많이 읽고, 빌드 실패의 가능성도 그 조회에 묶입니다.

레이아웃과 페이지의 revalidate 중 가장 짧은 값

revalidate 는 페이지에만 쓰는 것이 아니라 레이아웃에도 쓸 수 있습니다. 둘이 다르면 어떻게 될까요. 이전 모델의 캐싱 가이드는 규칙을 이렇게 적습니다.

The lowest revalidate across each layout and page of a single route will determine the revalidation frequency of the entire route.

한 경로를 이루는 레이아웃과 페이지 중 가장 짧은 값이 경로 전체의 주기가 됩니다. 레이아웃에 revalidate = 60 을 두면, 페이지에 3600 을 써도 60초마다 다시 만들어집니다. 레이아웃에 설정을 넣을 때는 그 아래 모든 페이지에 영향을 준다는 점을 기억해야 합니다.

같은 가이드에는 이 값이 정적으로 분석 가능해야 한다는 주의도 있습니다. revalidate = 600 은 되지만 revalidate = 60 * 10 은 안 됩니다. 빌드가 코드를 실행하지 않고 값을 읽기 때문입니다.

개발 환경에서는 캐시가 보이지 않는다

ISR 설정을 로컬에서 확인하려다 헷갈리는 이유도 문서에 있습니다.

In Development, Pages are always rendered on-demand and are never cached.

next dev 에서는 모든 페이지가 요청마다 렌더링되고 캐시되지 않습니다. 코드를 고치면 바로 보이게 하려는 것입니다. 그래서 revalidate 가 제대로 걸렸는지, 빌드가 어떤 페이지를 프리렌더하는지는 개발 서버로는 알 수 없습니다. next build 의 출력에서 경로마다 정적인지 동적인지 표시되는 것을 보거나, 배포한 뒤 응답 헤더로 확인해야 합니다.

경로마다 정할 두 가지

이제 경로 하나를 만들 때 두 가지를 따로 정하면 된다고 봅니다.

질문 설정
이 경로를 언제 처음 만드는가 (빌드 / 첫 요청 / 매번) 동적 세그먼트 여부, generateStaticParams 의 반환값, dynamic
만든 것을 얼마나 오래 쓰는가 revalidate, 태그 무효화

그리고 빌드 때 만드는 경로가 데이터베이스를 읽는다면, 빌드가 데이터베이스의 가용성에 묶인다는 사실을 받아들이거나 그 경로를 첫 요청 때 만들도록 옮깁니다. 이 블로그는 동적 세그먼트가 없는 목록 페이지는 빌드 때 만들고, 수가 늘어나는 동적 경로는 모두 첫 요청 때 만드는 쪽을 택했습니다. 빌드 실패는 직전 배포를 남기므로 다시 배포하면 되는 문제였습니다.

여기까지가 확실한 부분

ISR 에 빈 generateStaticParamsforce-static 이 필요하다는 점, ISR 재생성 때 generateStaticParams 가 다시 호출되지 않는다는 점, 가장 짧은 revalidate 가 경로 전체를 정한다는 점, 값이 정적으로 분석 가능해야 한다는 점, 개발 환경에서는 캐시되지 않는다는 점은 Next.js 16 문서의 이전 캐싱 모델 기준입니다. 빌드 로그와 재배포 결과는 2026년 9월 18일 이 블로그의 Vercel 배포에서 확인한 내용입니다. Cache Components 를 켠 프로젝트는 use cachecacheLife 로 같은 문제를 다르게 다룹니다.

이 게시글 공유하기

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.