Today I Learned

[Next.js] SSG, SSR, ISR 이해하기, 코드에 적용해보기

Seo Ji Won 2024. 4. 4. 23:34

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>
  );
}

 

1, 2, 3, 4.. 가 post Id

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