[Next.js] SSG, SSR, ISR 이해하기, 코드에 적용해보기
SSG
빌드 타임에 페이지를 만들어놓고 요청이 들어오면 만들어 둔 페이지를 전송해줌
SSR
사용자가 페이지를 요청할 때마다 서버에서 페이지를 새로 만들어서 줌
++ 여기서 서버는 Next.js 자체 웹 서버
ISR
SSG의 단점을 보완한 렌더링 방식
revalidate 시간이 10초라고 가정!
- 10초 동안 호출되는 모든 요청은 캐시된 데이터를 반환한다. => 10초동안 백만명이 접속해도 새로운 요청 X
- 10초가 지난 후 Next가 데이터를 성공적으로 가져오면 데이터 캐시를 새로운 데이터로 업데이트하고, 데이터를 가져오지 못하면 기존 데이터로 유지된다.
- 10초가 지나도 새 요청이 없으면 서버는 페이지를 자동으로 재생성하지 않는다. 다음 페이지 요청이 발생할 때까지 기다리고 10초가 지난 이후에 요청이 발생하면 해당 시점에 페이지를 재생성한다.
- SSG와 마찬가지로 generateStaticParams를 가질 수 있는데, revalidate 옵션으로 시간을 설정하면 그 주기에 맞추어 서버 사이드 데이터를 업데이트한다.
- fetch옵션에 revalidate를 주거나, 페이지 단위에서만 revalidate 옵션 적용 가능
generateStaticParams
generateStaticParams를 사용하면 동적 라우팅 페이지를 SSG로 만들 수 있다.
post 페이지로 이해해보자
app디렉토리 구조는 posts 페이지에서 post 아이디 값을 동적 라우팅으로 받아서 세부 페이지로 넘어가는 구조이다.
🔽 posts.tsx
// posts/page.tsx
import type { Post } from "@/types/PostProps";
import Link from "next/link";
export default async function PostPage() {
const response = await fetch("https://jsonplaceholder.typicode.com/posts");
const posts: Post[] = await response.json();
return (
<div className="min-h-screen bg-gray-100">
<main className="container mx-auto px-4 py-6">
{posts.length > 0 ? (
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
{posts.map((post: any) => (
<Link
href={`/posts/${post.id}`}
key={post.id}
className="rounded-lg bg-white p-6 shadow"
>
<h2 className="text-lg font-bold text-gray-800">
{post.title}
</h2>
<p className="text-gray-700">{post.body}</p>
</Link>
))}
</div>
) : (
<p className="text-gray-700">No posts found.</p>
)}
</main>
</div>
);
}
🔽 posts/[postId]/page.tsx
포스트 아이디를 받아서 페이지를 생성하는 [postId]/page.tsx 페이지는 generateStaticParams로 SSG페이지를 만든다.
즉 빌드 할 때 post의 id를 서버사이드에서 미리 가져오고, 그 아이디로 생성한 주소를 바탕으로 fetch하여 데이터를 가져와서 빌드 타임 때 페이지를 모두 만들어 두고 사용자가 요청하면 만들어둔 페이지를 전송해준다.
=> generateStaticParams 를 사용해서 pre-render 되기 원하는 path들을 명시해주는 것
// posts/[postId]/page.tsx
export async function generateStaticParams() {
const posts: Post[] = await fetch(
"https://jsonplaceholder.typicode.com/posts",
).then((res) => res.json());
return posts.map((post) => ({ // post Id를 빌드 때 미리 가져옴
postId: post.id.toString(),
}));
}
export default async function Post({ params }: { params: { postId: string } }) {
// 빌드 때 가져온 아이디로 생성된 URL에서 데이터를 fetch하여 빌드 타임에 페이지를 만들어둔다.
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts/${params.postId}`,
);
const post: Post = await response.json();
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-100">
<div className="w-1/2 rounded bg-white p-10 shadow-lg">
<h1 className="mb-4 text-4xl font-bold text-blue-600">{post.title}</h1>
<p className="text-gray-700">{post.body}</p>
</div>
<Link className="mt-4 hover:text-blue-500" href="/posts">
Go Back
</Link>
</div>
);
}
build했을 때 post Id 주소마다 모든 HTML 페이지가 만들어진 것을 out 폴더에서 확인할 수 있다.
빌드시 찍히는 로그에서도 각 post 아이디에 해당하는 페이지의 경로가 만들어진 것을 확인할 수 있다.
getServerSideProps
SSR페이지를 만들 때 사용할 수 있는 getServerSideProps는 마찬가지로 미리 서버 사이드에서 데이터를 받아와서 페이지를 만드는 것이다. 하지만 SSR은 사용자가 요청할때마다 새로 페이지를 만들어서 보내준다.
next 13 버전 이후로는 getServerSideProps의 기능을 no-cache 옵션으로 쉽게 구현 가능하지만
fetch 옵션을 사용할 수 없는 경우 getServerSideProps를 사용하거나 api route handler를 이용해서 커스텀 fetch 옵션을 주어서 SSR을 구현할 수 있다.
내 코드에서 이해해보자면
export default async function ReservePage({ searchParams }: { searchParams: { classId: string } }) {
const classId = searchParams.classId;
const classInfo = await fetchReserveClassInfo(classId);
return (
<div className="w-full h-full">
<h1 className="text-xl">예약하기</h1>
{classInfo ? (
<div className="flex w-full h-full bg-gray-200 p-6">
<DateTimePicker classDateList={classInfo.date} classTimeList={classInfo.time} />
<div className="flex flex-col justify-between items-center w-full p-6">
<ClassInfo classInfo={classInfo} />
<CurrentReserveQuantity classId={classInfo.classId} maxPeople={classInfo?.maxPeople} />
<PriceCalculator price={classInfo.price} classId={classInfo.classId} maxPeople={classInfo.maxPeople} />
<ReserveButton classId={classInfo.classId} maxPeople={classInfo.maxPeople} />
</div>
</div>
) : (
<p>클래스 정보를 불러오는데 오류가 발생했습니다. 잠시 후 다시 시도해주세요.</p>
)}
</div>
);
}
이 페이지는 searchParams를 쿼리스트링에서 동적으로 받아오고 있기 때문에 SSR 페이지가 된다.
SSG로 만들고 싶다면 generateStaticParams로 classId값을 넘겨주어서 빌드시에 각 클래스 id 페이지 주소를 만들고 데이터를 fetch하여 페이지를 만들어두면 각 클래스의 예약 페이지를 SSG로 보여줄 수 있다.
const reservationCompletePage = async ({ params }: { params: { reservationId: string } }) => {
const reservationId = decodeURIComponent(params.reservationId);
const reservationDetails = await fetchReservationDetails(reservationId);
if (!reservationDetails) {
return <div>예약 완료 정보를 불러오는 도중 문제가 발생했습니다.</div>;
}
const { class: classDetails, reserveDate, reserveTime, reserveQuantity, reservePrice } = reservationDetails;
const reserveInfoLabels = [
{
title: '클래스명',
description: `${classDetails.title}`
}// 중략
];
return (
<div className="w-full h-full">
<h1 className="text-xl">예약 완료</h1>
<div className="w-full h-full bg-gray-200 p-6 flex flex-col justify-between items-center">
<h1 className="text-xl text-center mb-20">예약이 정상적으로 처리되었습니다..</h1>
<div className="flex flex-col w-1/3 gap-6 mb-20">
{reserveInfoLabels.map(({ title, description }) => (
<div key={title} className="flex w-full justify-between gap-4">
<p className="w-20 text-right">{title}</p>
<p className="w-52 text-center">{description}</p>
</div>
))}
</div>
<NavigationButtons />
</div>
</div>
);
};
export default reservationCompletePage;
이 페이지는 사용자가 예약완료 버튼을 눌렀을 때, DB에 정보를 새로 생성하고, 그 생성된 예약 데이터의 고유 아이디를 응답으로 받아와서 동적 라우팅되는 페이지이다. 따라서 예약 버튼을 눌렀을 때 새로운 주소가 생성되고, 서버 사이드에서 어떤 주소로 값을 받아올지 예측할 수 없기 때문에 SSG로는 구현할 수 없다.
그럼 생겨나는 의문점
1. SSG 페이지에 변동사항을 반영하려면 build를 다시 해야하는 걸까? (O)
next의 ISR 공식문서를 보면
(ISR) enables you to use static-generation on a per-page basis, without needing to rebuild the entire site
(ISR)을 사용하면 전체 사이트를 다시 구축할 필요 없이 페이지별로 정적 생성을 사용할 수 있습니다 .
라고 나와있는 것을 보니 SSG 페이지에 변동사항을 반영하려면 build를 다시해야하는 것이 맞는 듯 하다.
그래서 변동사항이 있긴 하지만 적은 페이지는 revalidate 주고 ISR로 하는게 맞는 것 같다.
그럼 내가 구현하고 있는 예약하기 페이지는 수정되는 info가 실시간으로 반영될 필요는 없지만 사용자가 클래스를 수정할 때마다 rebuild를 할 수는없으니까 ISR이 적절해보인다. 또는 generateStaticParams로 일단 SSG로 만들고, revalidate를 줘서 ISR로 적용하는식?
++ 생각해보니 수정되는 정보를 실시간으로 보여주는게 맞다고 판단해서 SSR로 수정하였음
2. generateStaticParams로 구현된 SSG페이지에 revalidate 옵션을 주면 ISR인가? SSG인가?
= ISR
초기에는 SSG처럼 정적 페이지가 생성되지만, 이후 revalidate 주기마다 새로운 요청이 들어올 때 백그라운드에서 페이지가 갱신된다.
3. ISR은 SEO에 있어서 불리한가? (X)
next의 공식 문서를 참고하면 SSG = SSR > ISR >>>>> CSR 인 듯 하다
Learn Next.js | Next.js by Vercel - The React Framework
Next.js by Vercel is the full-stack React framework for the web.
nextjs.org
4. SSR 페이지인데 DB의 데이터가 변경되어도 새로고침했을 때 값이 변하지 않고 강력 새로고침(캐시 지우기)를 해야 바뀐 값이 보여지는 이유
- 알아보는 중..
https://www.jamesperkins.dev/post/page-reload-with-ssr/
🔽참고하기 좋은 글
pre-rendering 체크리스트: CSR SSR SSG ISR
페이지마다의 특징을 이해하고 렌더링 방식을 구분하는 것은 매우 중요합니다. 도대체 어떤 기준으로 선정해야할지 체크리스트와 함께 알아봅시다 ;) 개노답 3대장 CSR, SSR, SSG!! - 사견입니다
velog.io