icon
6장 : 데이터 페칭

정적 데이터 생성 (SSG)


Next.js는 뛰어난 성능을 제공하기 위해 다양한 렌더링 전략을 지원합니다. 그중 하나가 바로 정적 사이트 생성(Static Site Generation, SSG) 입니다. SSG는 빌드 시점에 페이지를 미리 HTML 파일로 생성하여 사용자 요청 시 서버에서 동적으로 렌더링할 필요 없이 즉시 제공하는 방식입니다. 이는 웹사이트의 로딩 속도를 획기적으로 개선하고, CDN(콘텐츠 전송 네트워크)을 통해 전 세계 사용자에게 매우 빠르게 콘텐츠를 전달할 수 있게 해줍니다.

이 절에서는 Next.js App Router에서 SSG를 구현하는 방법과 그 이점, 그리고 어떤 상황에서 SSG를 선택해야 하는지 자세히 알아보겠습니다.


SSG란 무엇인가요?

정적 사이트 생성(SSG) 은 웹 페이지의 모든 콘텐츠가 애플리케이션 빌드 타임(Build Time) 에 미리 생성되는 방식입니다. 이렇게 생성된 HTML, CSS, JavaScript 파일은 CDN에 배포되어 사용자 요청이 들어왔을 때 서버를 거치지 않고 바로 전송됩니다.

SSG의 주요 특징 및 이점

  • 최고의 성능: 페이지 요청 시 즉시 HTML 파일이 반환되므로, 데이터 페칭이나 서버 렌더링 과정이 필요 없어 매우 빠른 로딩 속도를 제공합니다.
  • 향상된 SEO: 검색 엔진 크롤러가 미리 생성된 HTML 콘텐츠를 쉽게 읽고 색인화할 수 있어 검색 엔진 최적화에 유리합니다.
  • 비용 효율성: 서버 부하가 거의 없거나 낮기 때문에, 트래픽이 많아도 서버 비용을 절감할 수 있습니다.
  • 안정성: 빌드 시 생성된 파일이므로 런타임 에러의 가능성이 적고, 정적 파일을 호스팅하는 CDN의 안정성에 따라 높은 가용성을 보장합니다.

언제 SSG를 사용해야 할까요?

  • 변화가 적은 데이터: 블로그 게시물, 문서, 상품 목록(재고 변동이 적은), 포트폴리오 사이트 등 콘텐츠가 자주 업데이트되지 않는 경우.
  • 모든 사용자에게 동일한 콘텐츠: 사용자별 맞춤형 콘텐츠가 필요하지 않고, 모든 사용자에게 동일한 페이지를 보여줄 때.
  • 높은 SEO 요구사항: 검색 엔진 노출이 중요한 경우.

App Router에서 SSG 구현하기

Next.js App Router에서 SSG를 구현하는 핵심은 동적 라우트([slug], [id] 등) 와 함께 generateStaticParams 함수를 사용하는 것입니다.

generateStaticParams 함수는 빌드 시점에 실행되며, 해당 동적 라우트에서 어떤 파라미터 값들을 사용하여 페이지를 미리 생성할지 Next.js에 알려줍니다.

기본적인 구현 단계

  1. 동적 라우트 폴더 생성: SSG할 페이지에 해당하는 동적 라우트 폴더(예: blog/[slug])를 만듭니다.
  2. page.tsx 파일 작성: 해당 폴더 안에 page.tsx 파일을 작성하고, params prop을 통해 동적인 값을 받아 데이터를 페칭하고 UI를 렌더링합니다.
  3. generateStaticParams 함수 작성: page.tsx (또는 layout.tsx) 파일 내부에 generateStaticParams라는 async 함수를 정의합니다. 이 함수는 미리 생성할 경로의 파라미터 객체 배열을 반환해야 합니다.

실습: 블로그 게시물 SSG

이전 5장 1절에서 만들었던 src/app/posts/[id]/page.tsx 예제를 사용하여, 이 게시물 상세 페이지들을 빌드 시점에 미리 생성하도록 설정해 보겠습니다.

src/app/posts/[id]/page.tsx
// src/app/posts/[id]/page.tsx

interface PostDetailPageProps {
  params: {
    id: string; // URL에서 추출될 게시물 ID
  };
}

interface Post {
  id: number;
  title: string;
  body: string;
}

// 1. 특정 게시물 데이터를 가져오는 함수 (서버 컴포넌트 내부에서 사용)
async function getPost(id: string): Promise<Post> {
  const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${id}`);
  if (!res.ok) {
    // 실제 서비스에서는 적절한 에러 처리 또는 notFound() 호출
    throw new Error(`Failed to fetch post with ID: ${id}`);
  }
  return res.json();
}

// 2. generateStaticParams 함수 정의: 빌드 시 SSG할 경로들을 결정합니다.
// 이 함수는 'app' 폴더 내의 동적 라우트 페이지/레이아웃에서만 사용할 수 있습니다.
export async function generateStaticParams() {
  console.log('generateStaticParams 🚀: fetching all posts IDs for SSG'); // 빌드 시에만 이 로그가 보입니다.
  const res = await fetch('https://jsonplaceholder.typicode.com/posts');
  const posts: Post[] = await res.json();

  // 모든 게시물의 ID를 추출하여 { id: string } 형태의 객체 배열로 반환합니다.
  // Next.js는 이 배열의 각 객체에 대해 해당 페이지를 미리 생성합니다.
  return posts.map((post) => ({
    id: post.id.toString(), // params의 값은 항상 문자열이어야 합니다.
  }));
}

// 3. 페이지 컴포넌트: params를 받아 데이터를 렌더링합니다.
export default async function PostDetailPage({ params }: PostDetailPageProps) {
  const { id } = params;
  const post = await getPost(id); // getStaticProps의 context.params와 유사하게 작동

  return (
    <div style={{ padding: '20px', maxWidth: '800px', margin: '0 auto', border: '1px solid #eee', borderRadius: '8px', boxShadow: '0 2px 5px rgba(0,0,0,0.05)' }}>
      <h1 style={{ color: '#333', marginBottom: '15px' }}>{post.title}</h1>
      <p style={{ lineHeight: '1.6', color: '#555' }}>{post.body}</p>
      <div style={{ marginTop: '30px', paddingTop: '15px', borderTop: '1px dashed #eee' }}>
        <Link href="/posts">
          <a style={{ color: '#0070f3', textDecoration: 'none' }}>&larr; 목록으로 돌아가기</a>
        </Link>
      </div>
    </div>
  );
}

SSG 테스트 방법

  1. 빌드 실행: 터미널에서 다음 명령어를 실행하여 Next.js 애플리케이션을 빌드합니다.

    npm run build
    # 또는
    yarn build

    빌드 과정에서 generateStaticParams 함수가 실행되어 console.log 메시지가 터미널에 출력되는 것을 볼 수 있습니다. 빌드 완료 후에는 out 또는 .next 폴더에 미리 생성된 HTML 파일들이 존재하게 됩니다.

  2. 프로덕션 모드에서 실행: 빌드된 정적 파일들을 서빙하기 위해 다음 명령어를 실행합니다.

    npm run start
    # 또는
    yarn start
  3. 브라우저 확인: http://localhost:3000/posts/1 또는 http://localhost:3000/posts/10과 같은 URL로 접속해 보세요. 페이지가 매우 빠르게 로드되는 것을 경험할 수 있습니다. 이는 서버에서 동적으로 데이터를 가져와 렌더링하는 과정 없이, 미리 생성된 HTML 파일을 CDN이 바로 전달하기 때문입니다.


generateStaticParams의 작동 방식

  • 빌드 시 실행: generateStaticParamsnpm run build 명령이 실행될 때만 한 번 실행됩니다. 개발 서버(npm run dev)에서는 요청 시점에 동적으로 파라미터를 처리합니다.
  • 데이터 페칭: generateStaticParams 내부에서도 fetch와 같은 비동기 데이터 페칭 함수를 사용할 수 있습니다. 여기에서 가져온 데이터는 빌드 시점에 사용됩니다.
  • 반환 값: 함수는 { paramName: value } 형태의 객체 배열을 반환해야 합니다. paramName은 동적 라우트 폴더의 대괄호 안 이름([id]id)과 정확히 일치해야 하며, valuestring 타입이어야 합니다.
  • Catch-all 세그먼트 ([...slug]): Catch-all 세그먼트의 경우, slug: ['path', 'to', 'document']와 같이 문자열 배열을 반환해야 합니다.
    export async function generateStaticParams() {
      return [{ slug: ['a', 'b'] }, { slug: ['c'] }];
    }
  • 성능 최적화: generateStaticParams에서 반환하는 경로의 수가 많을수록 빌드 시간이 길어집니다. 따라서 실제 필요한 페이지만 SSG하거나, 중요한 페이지 위주로 SSG하고 나머지는 CSR 또는 SSR로 처리하는 전략을 고려할 수 있습니다.

SSG와 ISR

generateStaticParams를 통해 SSG된 페이지는 기본적으로 빌드 시점에 고정됩니다. 하지만 fetchnext.revalidate 옵션을 사용하면, SSG된 페이지를 배포 후에도 주기적으로 백그라운드에서 재검증(revalidate)하여 최신 데이터를 반영할 수 있습니다. 이것이 바로 ISR(Incremental Static Regeneration) 입니다.

ISR은 SSG의 빠른 응답 속도와 서버 렌더링의 데이터 신선도 장점을 결합한 강력한 전략입니다.

src/app/products/[id]/page.tsx
// src/app/products/[id]/page.tsx (ISR 예시)

// ... (다른 코드 생략) ...

async function getProduct(id: string): Promise<Product> {
  const res = await fetch(`https://api.example.com/products/${id}`, {
    // 이 페이지는 SSG로 미리 생성되지만,
    // 60초마다 백그라운드에서 데이터를 재검증하여 업데이트합니다.
    next: { revalidate: 60 },
  });
  if (!res.ok) { /* ... */ }
  return res.json();
}

export async function generateStaticParams() {
  // 빌드 시점에 생성할 모든 상품 ID를 반환
  // ... (모든 상품 ID를 가져오는 로직) ...
  return [{ id: 'product-1' }, { id: 'product-2' }];
}

export default async function ProductDetailPage({ params }: { params: { id: string } }) {
  const product = await getProduct(params.id);
  // ... (상품 정보 렌더링) ...
}

위 예시에서 generateStaticParams는 빌드 시점에 페이지를 생성하고, fetch 내의 next: { revalidate: 60 }는 해당 페이지가 배포된 후에도 60초마다 최신 데이터로 업데이트될 수 있도록 백그라운드 재검증을 트리거합니다.

정적 사이트 생성(SSG)은 Next.js 애플리케이션의 성능을 극대화하는 강력한 방법입니다. generateStaticParams를 사용하여 빌드 시점에 동적 페이지를 미리 생성하고, 필요에 따라 ISR을 통해 데이터의 신선도를 유지함으로써 사용자에게 빠르고 효율적인 웹 경험을 제공할 수 있습니다.