s e o p p o r t . l o g

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를 다시 해야하는 걸까? (맞음)

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로 적용하는식?

 

2. generateStaticParams로 구현된 SSG페이지에 revalidate 옵션을 주면 ISR인가? SSG인가?

 

 

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

 

 

하 드디어 이해했다...