Static Site Generation (SSG) - Page Router

SSG는 빌드 시점에 서버가 HTML을 미리 만들어두고,
배포 이후에는 요청이 들어오면 이미 생성된 HTML을 그대로 응답하는 렌더링 방식이다.
getStaticProps
getStaticProps가 존재하면 해당 페이지는 SSG로 동작한다.
export const getStaticProps = async () => {
const books = await fetchBooks();
return {
props: {
books,
},
};
};- 빌드 시점에 한 번만 실행된다.
- 브라우저에서는 실행되지 않는다.
- 반환한
props는 페이지 컴포넌트에 전달된다.
SSG 페이지의 실행 흐름
- 빌드 시점
- 서버에서
getStaticProps실행 - HTML 생성
- 배포 이후
- 요청 시 이미 생성된 HTML을 그대로 응답
- JS 로드 후
Hydration수행
SSR과 달리 요청마다 서버 코드가 실행되지 않는다.
Next.js 빌드 결과에서는 각 페이지가 정적, SSG, 또는 동적 SSR로 분류되어 출력된다.

장점
- 빌드 타임에 렌더링 비용이 들어가더라도, 사용자 요청에는 매우 빠르게 응답할 수 있다.
- 서버 부하가 적고 캐싱/CDN과 궁합이 좋다.
단점
- 항상 같은 HTML을 응답하기 때문에 데이터 변경이 바로 반영되지 않는다.
- 최신 데이터가 필요하면 재빌드가 필요하다(→ 이 단점을 보완하는 방식이 ISR)
SSG에서 실제 API 데이터 페칭
SSG에서도 SSR처럼 서버에서 API를 호출해 데이터를 가져올 수 있다.
차이점은 요청 시점이 아니라 빌드 시점에 한 번만 실행된다는 것이다.
export const getStaticProps = async () => {
console.log('인덱스 페이지 빌드 중');
const [allBooks, recoBooks] = await Promise.all([
fetchBooks(),
fetchRandomBooks(),
]);
return {
props: {
allBooks,
recoBooks,
},
};
};
export default function Home({
allBooks,
recoBooks,
}: InferGetStaticPropsType<typeof getStaticProps>) {
return (
<div className={style.container}>
<section>
<h3>지금 추천하는 도서</h3>
{recoBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
<section>
<h3>등록된 모든 도서</h3>
{allBooks.map((book) => (
<BookItem key={book.id} {...book} />
))}
</section>
</div>
);
}getStaticProps는 빌드 시점에 한 번만 실행된다.- 배포 후 서버는 이 함수를 다시 실행하지 않는다.
console.log는 빌드 로그에서만 확인할 수 있다.
컴포넌트 자체는 SSR, SSG, CSR 모두 동일하게 생길 수 있다.
차이는 데이터를 언제, 어디서 준비하느냐에 있다.
SSG의 한계: 빌드 타임에 확정할 수 없는 데이터
SSG는 빌드 시점에 데이터를 확정해야 한다.
따라서 요청 시점에 값이 결정되는 페이지에는 구조적으로 한계가 있다.
- 검색 페이지 (
?q=react처럼 쿼리에 따라 결과가 달라지는 경우) - 사용자 입력에 따라 결과가 달라지는 페이지
- 로그인 사용자별로 다른 데이터를 보여주는 페이지
검색 페이지는 CSR로 처리해야 한다
검색 페이지는 ?q=... 값이 사용자 요청 시점에 결정되기 때문에, SSG로는 검색어를 미리 알 수 없다. 이런 경우 클라이언트에서 쿼리 스트링을 읽고 useEffect로 데이터를 가져오는 CSR 방식이 적합하다.
import SearchableLayout from '@/components/searchable-layout';
import { ReactNode, useEffect, useState } from 'react';
import BookItem from '@/components/book-items';
import fetchBooks from '@/lib/fetch-books';
import { useRouter } from 'next/router';
import { BookData } from '@/types';
export default function Page() {
const [books, setBooks] = useState<BookData[]>([]);
const router = useRouter();
const q = router.query.q;
const fetchSearchResult = async () => {
const data = await fetchBooks(q as string);
setBooks(data);
};
useEffect(() => {
if (q) {
fetchSearchResult();
}
}, [q]);
return (
<div>
{books.map((book) => (
<BookItem key={book.id} {...book} />
))}
</div>
);
}
Page.getLayout = (page: ReactNode) => {
return <SearchableLayout>{page}</SearchableLayout>;
};검색 페이지의 초기 HTML (CSR)

검색어는 브라우저에서 결정되므로, 서버에서 내려오는 HTML에는 검색 결과가 포함되지 않는다.
- 빌드 타임에는 페이지의 기본 HTML만 생성된다.
- 실제 검색 데이터는 브라우저에서 fetch되어 렌더링된다.
즉, 이 페이지는 화면 구조는 SSG로 제공되고, 검색 결과는 CSR로 처리된다.
SSG는 빌드 타임에 확정 가능한 화면을 빠르게 제공하는 데 적합하고,
확정할 수 없는 데이터는 SSR이나 CSR로 분리해 처리해야 한다.
동적 경로에 SSG 적용하기
getStaticPaths
getStaticPaths는 빌드 시점에 어떤 경로를 미리 생성할지 정의하는 함수이다.
export const getStaticPaths = () => {
return {
paths: [
{ params: { id: '1' } },
{ params: { id: '2' } },
{ params: { id: '3' } },
],
fallback: false, // "blocking" / true
};
};paths: 빌드 타임에 생성할 경로 목록fallback: paths에 없는 경로 요청 시 동작 방식
Fallback 옵션
fallback: false
- paths에 정의되지 않은 경로는 모두 404 반환
- 가장 단순하고 예측 가능한 방식
모든 페이지를 빌드 타임에 확정할 수 있을 때 사용한다.
fallback: true
paths에 정의되지 않은 경로가 요청되면 다음 흐름으로 동작한다.
- props 없는 fallback 페이지 먼저 렌더링
- 백그라운드에서 HTML 생성
- 생성 완료 후 실제 페이지로 교체
즉, 일단 페이지부터 보여주고 나중에 완성하는 방식이다.
이 경우 페이지 컴포넌트에서는 fallback 상태 처리가 필요하다.
const router = useRouter();
if (router.isFallback) return '로딩중입니다.';router.isFallback은 현재 페이지가 fallback 상태인지 여부를 나타낸다.- fallback 상태란 페이지는 렌더링되었지만 props 데이터가 아직 없는 상태를 의미한다.
fallback: 'blocking'
paths에 없는 경로 요청 시,
- 서버가 HTML 생성이 완료될 때까지 대기
- 완성된 페이지를 한 번에 응답
사용자 입장에서는 SSR처럼 동작하지만, 생성된 페이지는 이후 정적으로 캐싱된다.
즉, 최초 요청만 SSR 느낌 → 이후에는 SSG