Next.js App Router: Dynamic Route에서 generateStaticParams 활용법

Next.js App Router generateStaticParams 활용법 및 정적 빌드 가이드
3초 요약: 이렇게 고치세요
  • 원인: [id] 같은 동적 세그먼트는 generateStaticParams가 없으면 매 요청마다 서버가 다시 렌더링해서 트래픽이 늘수록 부하도 늘어남
  • 해결: 빌드 시점에 {'{'} id {'}'} 배열을 반환하는 generateStaticParams를 정의해 해당 경로들을 정적 HTML로 미리 생성
  • 주의: 목록/상세를 둘 다 fetch하면 빌드 시 중복 요청이 쌓일 수 있는데, Next.js의 fetch 메모이제이션이 같은 요청은 자동으로 합쳐줌
export async function generateStaticParams() {
  return posts.map((p) => ({ id: p.id.toString() }));
}
↓ 왜 이런 에러가 나는지 원리 보기

Next.js App Router에서 /blog/[id]처럼 동적 세그먼트를 쓰는 페이지는 기본적으로 요청이 들어올 때마다 서버에서 다시 렌더링됩니다. 트래픽이 늘어나면 그만큼 서버 부하와 응답 지연도 함께 늘어나는 구조입니다. 이 문제를 빌드 시점에 미리 해결하는 방법이 generateStaticParams이며, 이 글에서는 이 API의 동작 원리와 실무에서 주의해야 할 지점을 정리합니다.


Dynamic SSR vs Static SSG의 차이점 및 필요성

기존 Next.js Pages Router 환경에서는 getStaticPaths 함수를 사용하여 동적 페이지의 정적 생성을 유도했습니다. App Router 구조로 개편되면서 Next.js는 이를 더 단순화한 형태의 generateStaticParams API로 대체했습니다. 이 함수를 정의하지 않은 동적 페이지(/products/[id]/page.tsx)는 구글 봇이나 일반 유저가 접속할 때마다 Node.js 백엔드 서버가 DB를 매번 새로 찌르고 리액트 컴포넌트 트리를 새로 연산하여 HTML을 조립하게 됩니다.

반면, generateStaticParams가 추가되면 Next.js는 배포 빌드 단계(npm run build)에서 이 함수를 먼저 실행하여 반환된 배열 데이터(예: id: '1', '2', '3')를 수집합니다. 수집된 인자들을 바탕으로 각각의 독립된 HTML 파일을 디스크에 미리 쏟아내는 정적 사이트 생성(SSG) 방식으로 레이아웃을 고정해 둡니다. 그 결과, 실제 유저가 서비스를 로드할 때는 서버의 CPU 연산 없이 전 세계 엣지 서버(CDN)에 올라간 캐시 리소스가 즉시 뿌려지므로, 네트워크 대기 시간이 크게 줄어듭니다.

간단히 비교하면, 일반적인 동적 라우팅은 유저가 접속할 때마다 API·DB를 호출하고 컴포넌트를 실시간으로 렌더링하기 때문에 요청마다 0.5~2초 안팎의 대기 시간이 발생합니다. 반면 generateStaticParams로 빌드 타임에 HTML을 미리 만들어두면 유저 요청 시 CDN이 즉시 캐시를 반환하므로 대기 시간이 0.05초 미만으로 줄어듭니다.


실무에서 마주한 중복 페칭(Request Duplication) 제어

상품 상세 페이지 수가 많은 커머스 서비스처럼 동적 세그먼트가 대량인 프로젝트에서는, 공식 레퍼런스대로 generateStaticParams 안에서 목록 API를 페칭하고 본문 ProductPage 컴포넌트에서 상세 API를 또다시 페칭하도록 구성하면 빌드 시점에 중복 네트워크 요청이 과도하게 쌓여 배포 인스턴스에 부담을 주는 경우가 흔합니다.

이런 상황에서 도움이 되는 장치는 Next.js가 기본으로 지원하는 fetch 데이터 캐싱 및 메모이징(Request Memoization) 기능을 명시적으로 활용하는 것입니다. 또한 정해진 범위 밖의 URL로 유저가 신규 접근했을 때 동적 서버 렌더링으로 자연스럽게 통과시킬지, 혹은 404로 차단할지를 export const dynamicParams = true 플래그로 선언해 두면 빌드 부담과 배포 안정성을 함께 관리할 수 있습니다.

상품이 몇만 개인 커머스 사이트에서 generateStaticParams를 처음 도입했을 때, 빌드 시간이 예상보다 훨씬 오래 걸려서 원인을 찾아보니 목록 API와 상세 API를 캐싱 없이 각각 따로 부르고 있었습니다. fetch에 태그를 달아 메모이제이션이 되도록 정리하고 나서야 빌드 시간이 눈에 띄게 줄었습니다.


실전 활용: generateStaticParams 구현 코드

비동기 API 데이터를 수집해 정적 빌드를 수행하는 기본 파일 작성 예시입니다. (app/posts/[id]/page.tsx)

import { notFound } from 'next/navigation';

interface Post {
  id: string;
  title: string;
  content: string;
}

// 빌드 시 미리 선언해 둔 staticParams 외에 런타임에 유입된 파라미터 처리 제어
// true: 빌드 외 인자는 온디맨드 실시간 SSR 생성 처리
// false: 빌드 외 인자는 즉각 404 Not Found 반환
export const dynamicParams = true;

// 1. 공통 비동기 데이터 헬퍼 함수 (Next.js 캐시가 자동으로 이중 호출을 병합함)
async function getPostData(id: string): Promise<Post | null> {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { revalidate: 3600 } // 1시간 캐싱
  });
  if (!res.ok) return null;
  return res.json();
}

// 2. 핵심: 빌드 타임에 static HTML로 구워낼 파라미터 리스트 수집 선언
export async function generateStaticParams() {
  const res = await fetch('https://api.example.com/posts/list');
  const posts: Post[] = await res.json();

  // 반드시 [{ id: '1' }, { id: '2' }] 와 같은 파라미터 매핑 배열 객체를 반환해야 함
  return posts.map((post) => ({
    id: post.id.toString(),
  }));
}

// 3. 실제 페이지 렌더링 컴포넌트
export default async function PostDetailPage({ params }: { params: { id: string } }) {
  const post = await getPostData(params.id);

  if (!post) {
    notFound(); // 404 리다이렉트
  }

  return (
    <article style={{ padding: '20px', fontFamily: 'sans-serif' }}>
      <h2 style={{ fontSize: '24px', color: '#1e293b' }}>{post.title}</h2>
      <p style={{ marginTop: '15px', color: '#334155', lineHeight: '1.6' }}>{post.content}</p>
    </article>
  );
}

해결 코드의 라인별 동작 해설

export const dynamicParams = true 속성은 사전에 정의되지 않은 새 포스팅이 등록되어 유저가 진입했을 때, 에러 대신 Next.js 서버가 그 즉시 서버 사이드 렌더링으로 대응하여 캐시 목록에 추가하도록 돕는 유연성 제약 규칙입니다. 핵심인 generateStaticParams() 함수는 배포 빌드가 시작될 때 비동기로 원격 서버로부터 글 목록을 파싱한 후, 동적 변수 슬롯인 [id] 구조에 맞춰 { id: "포스트ID" } 형태의 리터럴 객체 배열을 뽑아 반환합니다.

여기서 많은 개발자분들이 우려하는 '목록 데이터 호출'과 '상세 데이터 호출' 간의 이중 네트워크 낭비 문제는 Next.js의 기본 fetch 중복 방지 캐시(Request Memoization) 장치가 런타임 레이어에서 잡아줍니다. 따라서 PostDetailPage 컴포넌트 내의 getPostData()는 다중 호출되더라도 실제로 네트워크를 재차 관통하지 않고 메모리에 캐싱된 버퍼를 즉각 호출하므로 응답 지연을 줄여줍니다.


요약: Pages Router (getStaticPaths) vs App Router (generateStaticParams)

기존 라우터 방식과 App Router 방식 간의 차이를 정리했습니다.

구분 항목 getStaticPaths (이전 Pages) generateStaticParams (App Router)
반환 객체 형태 { paths: [{ params: { id: '1' } }], fallback: true } 구조로 다소 복잡함 [{ id: '1' }] 형태의 단순한 파라미터 객체 배열 반환
예외 주소 접근 대응 fallback: true / 'blocking' 제어로직 바인딩 dynamicParams = true / false 상수 명시
다중 슬롯 세그먼트 루프 구조가 무거워질수록 내부 맵핑 가독성이 저하됨 다중 폴더 구조([category]/[id])도 평면 객체로 쉽게 표현
추가 추천 자료 및 공식 레퍼런스

Next.js App Router의 공식 Static Params 함수 세부 설정 옵션은 아래 문서에서 확인할 수 있습니다.

호마다의 웹개발 팁

참고로 generateStaticParams로 미리 구워둔 정적 페이지는 데이터가 바뀌어도 자동으로 갱신되지 않습니다. revalidate 주기나 온디맨드 재검증(on-demand revalidation)을 어떻게 가져갈지는 프로젝트 성격에 맞춰 별도로 설계해야 합니다. 캐시 무효화 개념이 낯설다면 [호마다의 IT 개발 입문 블로그] 의 캐싱 기초 글도 함께 참고해보세요.

댓글

댓글 쓰기

이 블로그의 인기 게시물

TypeScript: Unknown vs Any 타입 차이점과 안전한 타입 가드(Type Guard)

Next.js 하이드레이션 오류 원인과 해결법 완벽 정리

Next.js App Router에서 getServerSideProps 대체 및 데이터 페칭법