[Next.js] Page Router - 렌더링 (2) SSG

그릿 Grit·2025년 4월 6일
0

next.js

목록 보기
5/5
post-thumbnail

🌱 해당 포스트는 한 입 크기로 잘라먹는 Next.js(v15)을 수강하고, Next.js 공식 문서 - page router를 참고하여 정리한 글입니다.

들어가며

기존의 SSR(서버 사이트 렌더링)의 경우, 사전 렌더링 과정에서 데이터가 필요하다면 fetching해 오는데, 시간이 오래 걸리면 응답이 느려진다는 단점이 있었다.

이런 단점을 해결할 수 있는 게 바로 SSG 렌더링 방식이다.

SSG 방식에서는 접속 요청이 들어왔을 때가 아닌, 빌드 타임에 페이지를 미리 사전 렌더링 해둠으로써, 문제를 해결한다. Next.js 프로젝트에서는 npm run build 시 사전 렌더링을 진행하게 된다. 데이터 요청도 서버가 가동되기 이전인 빌드 타임에만 일어나기 때문에, 빌드 타임 이후에 발생하는 접속 요청들에 대해서는 굉장히 빠른 속도로 응답할 수 있다.

Static Site Generation (SSG, 정적 사이트 생성)

작동 방식


SSG는 빌드 타임에 사전 렌더링을 하여 페이지를 미리 생성하고, 이후에는 더 이상 새롭게 페이지를 생성하지 않는다. 따라서, 사용자의 접속 요청이 있을 때마다, 같은 페이지를 반환한다.

장점


사전 렌더링에 많은 시간이 소요되더라도, 이후 사용자 요청에 빠르게 응답 가능하다.

단점

SSG 방식은 빌드 타임 이후에는 사전 렌더링을 하지 않기 때문에, 매번 똑같은 페이지를 응답한다.
[상황별 적합 여부]

  • 최신 데이터가 빠르게 반영되어야 하는 페이지 (❌)
  • 데이터가 자주 업데이트 되지 않는 정적 페이지 (✅)

SSG 사용법

정적 경로에 SSG 적용하기

// SSG 방식으로 작동
export const getStaticProps = async (context: GetStaticPropsContext) => {
  const [allBooks, recoBooks] = await Promise.all([
    fetchBooks(),
    fetchRandomBooks(),
  ]);

  return {
    props: {
      allBooks,
      recoBooks,
    },
  };
};

SSG로 데이터를 fetching 해오기 위해서, getStaticProps 함수를 사용하면 된다. getStaticProps 함수가 있으면, SSG로 동작한다.
context의 타입은 GetStaticPropsContext이다.

// props의 타입 살펴보기
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>
  );
}

props의 타입은 InferGetStaticPropsType<typeof getStaticProps>이 된다.

SSG 적용이 불가능한 경우) 쿼리 스트링이 필요한 경우

Next.js - getStaticProps API 공식문서를 살펴보면, getStaticProps 함수의 인자로 주어지는 context 안에 query가 없는 것을 확인할 수 있다.

사실, 빌드 타임에 query string을 알 수가 없는 것이 당연하기 때문에, context 안에 query가 없는 것은 합리적이다.
따라서 쿼리 스트링을 필요로 하는 search 페이지 같은 경우는 SSG 방식으로 동작시킬 수가 없다. 엄밀히 말하면, 검색 결과를 서버로부터 불러오는 동작은 수행할 수 없다.

만약 SSG로 search 페이지를 동작시키고 싶다면, SSG+CSR로 구현해야한다.

현재 쿼리 스트링을 꺼내와서 해당 값을 기준으로 검색 결과 데이터를 불러오는 과정을 사전 렌더링 이후에 컴포넌트 페이지 역할을 하는 컴포넌트에서 직접 진행해야 한다 (기존 react CSR). 이를 구현하는 방법은 간단하다. useRouter 훅을 사용하여 쿼리 스트링을 받아오고, useEffect를 사용하여 데이터 페칭을 하며, 데이터는 useState를 통해 관리하면 된다..

따라서 사전 렌더링 과정에서는, 결국 이 페이지의 레이아웃(div 태그 정도만) 정도만 렌더링하게 된다. 그 후, 클라이언트 사이드 측에서 이 컴포넌트가 다시 실행되면서, 직접 쿼리 스트링으로 검색어를 불러와서 검색 결과 데이터를 클라이언트 사이드 측에서 렌더링하게 된다.


search 페이지를 들어가보면, 검색 결과 데이터는 제외하고 나머지 부분만 렌더링해서 브라우저에게 보내주는 것을 확인할 수 있다.

동적 경로에 SSG 적용하기

동적인 경로에 대해서 SSG를 적용하고 싶다면, 반드시 getStaticPaths를 통해 어떤 url 파라미터가 존재할 수 있는지, 즉 어떤 경로가 존재할 수 있는지 알아야 한다.

getStaticPaths를 사용하지 않고 동적 경로에 SSG를 적용하려고 하면 아래와 같은 에러가 뜬다.

경로를 설정하게 되면, 가능한 경로에 대한 html을 모두 생성하게 된다.

getStaticPaths 사용법

export const getStaticPaths = () => {
  return {
    // paths라는 속성으로 어떤 경로가 존재할 수 있는지를 배열로 반환
    paths: [
      {
        // 가능한 경로를 params라는 속성에 적어주면 된다.
        params: {
          // 파라미터 이름 : 값
          // 파라미터 값은 반드시 문자열로 작성해야지 정상 작동한다.
          id: "1",
        },
      },
      { params: { id: "2" } },
      { params: { id: "3" } },
    ],
    // fallback은 예외 상황에 대비하는 옵션
    // 존재하지 않는 url에 어떻게 대응할 것인지의 옵션
    fallback: false, // not found를 반환함
  };
};

가능한 경로를 paths 속성에 배열로 반환한다.
배열에는 {params : {[파라미터 이름] : 문자열 경로 값}} 을 작성한다.
fallback 옵션을 반드시 넣어주어야 한다. fallback 옵션은 존재하지 않는 url에 어떻게 대응할 것인지의 옵션으로 기본은 false이다. false 이면, 404.tsx(not found) 페이지를 반환한다.

fallback 옵션

fallback 옵션은 존재하지 않는 url에 대해 어떻게 대응해줄 것인지를 결정해준다.

  • fallback: false : 404 Not Found 페이지를 반환한다.
  • fallback: "blocking" : 매칭되지 않았던 경로의 페이지를 즉시 생성한다. (SSR 처럼)
  • fallback: true : 매칭되지 않았던 경로의 페이지를 즉시 생성하되, 페이지만 미리 반환하고, props를 후속으로 보내준다.

fallback: false

404 Not Found 페이지를 반환한다.

fallback: "blocking"

SSR 방식처럼 실시간으로 요청받은 페이지를 사전 렌더링 해서 브라우저에게 반환해준다.

따라서, blocking 옵션을 이용하면 빌드 타임에 사전에 생성해 두지 않았던 페이지까지 사용자에게 제공해줄수 있다는 장점이 있다.

이후 접속 요청이 들어와 생성된 페이지는 서버에 자동으로 저장된다.

fallback: true

fallback: "blocking" 방식을 사용하여, 존재하지 않았던 페이지를 SSR 방식으로 새롭게 생성할 때, 만약 백엔드 서버로의 추가적인 데이터를 요청 등으로 인해 페이지의 생성 시간(사전 렌더링)이 길어지면 서버가 응답을 아무것도 하지 않기 때문에 문제가 발생할 수 있다.
이런 문제를 해결할 수 있는 방법이 fallback: true 옵션이다.

이 방식은, fallback: "blocking" 처럼, 존재하지 않았던 페이지를 SSR 방식으로 새롭게 생성하지만, props가 없는 상태로 페이지를 반환하고(사실상 레이아웃이 그려진다), 이후에 props를 전달해주는 방식으로 페이지를 렌더링 한다.

✨ 여기서 의미하는 Props란?
페이지에 필요한 데이터를 가져오는 getStaticProps 함수가 페이지 컴포넌트에 전달해주는 props

예시

export const getStaticPaths = () => {
  return {
    // paths라는 속성으로 어떤 경로가 존재할 수 있는지를 배열로 반환
    paths: [
      { params: { id: "1" } },
      { params: { id: "2" } },
      { params: { id: "3" } },
    ],
    // fallback: false, // not found를 반환함
    // fallback: "blocking",
    fallback: true,
  };
};

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const book = await fetchOneBook(Number(id));
  return { props: { book } };
};

export default function Book({
  book,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  if (!book) {
    return "문제가 발생했습니다. 다시 시도해주세요";
  }

  const { id, title, subTitle, description, author, publisher, coverImgUrl } =
    book;

  return (
    <div className={style.container}>
     /* 코드 생략 */
    </div>
  );
}

위 코드에서 보면, fallback: "blocking" 으로 옵션을 주었기 때문에, 초기 접속시 props가 전달되지 않아,

 if (!book) {
    return "문제가 발생했습니다. 다시 시도해주세요";
  }

부분이 렌더링 되었다.

그리고 이후에 props를 json파일으로 받아 렌더링한다.

💡 내 생각
페이지 생성이 모든 초기 접속에 대해 새로 생성하는 것이 아닌 next 서버 기준 완전 초기에만 페이지를 생성하고, 그 다음부터는 .next에 페이지를 저장하고 그 페이지를 내려보내주는 것 같다.

fallback: true 에서 fallback 상태 예외 처리

✨ fallback 상태란?
페이지 컴포넌트가 아직 서버로부터 데이터(getStaticProps에서의 props)를 전달 받지 못한 상태를 말한다.

현재 예시 코드에서 fallback 상태에 있으면, if(!book) 구문에 걸려 "문제가 발생했습니다."로 뜨게 된다. 아직 로딩 중인 건데, "오류가 발생해서 데이터가 안뜨는 거구나" 라고 사용자가 오해할 수 있다. 따라서 제대로 된 상태를 표시할 필요가 있다.

그렇다고, 현재 코드의 텍스트를 if(!book) 구문에 걸려 "로딩중입니다." 라 바꿔버리면, 실제로 데이터를 가져오는데 오류가 뜬 상황에서도 "로딩중입니다."이라고 뜨기 때문에 문제가 있다.

따라서 fallback 상태에서만 보여주는 UI가 필요하다

export default function Book({
  book,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  const router = useRouter();

  // fallback 상태에 해당할때만 보여지는 ui
  if (router.isFallback) return "로딩중";

/* ...코드 생략... */

props 데이터가 없는 경우, Not Found 반환

export const getStaticProps = async (context: GetStaticPropsContext) => {
  const id = context.params!.id;
  const book = await fetchOneBook(Number(id));

  if (!book) {
    return {
      notFound: true,
    };
  }

  return { props: { book } };
};

getStaticPropsprops가 없는 경우,{notFound: true}를 리턴하여, Not Found 페이지를 반환할 수 있다.

빌드 결과 살펴보기


먼저, getStaticPaths 로 설정해준 것과 같이, id1, 2, 3에 대한 경로의 페이지(html)이 생성된 것을 볼 수 있다.

● (SSG) prerendered as static HTML (uses getStaticProps)

이 페이지가 SSG 방식으로 동작. HTML로 사전 렌더링된 페이지 (getStaticProps를 사용해서 데이터를 불러오는 static 페이지를 의미)

ƒ (Dynamic) server-rendered on demand

동적(다이나믹) 페이지를 의미 -> 요청을 받을 때마다 사전 렌더링된다. or 실행된다.
API routes도 ssr로 작동하도록 설정되어 있기 때문에 ƒ라고 쓰여있다.

○ (Static) prerendered as static content

SSG와 동일한 정적 페이지지만, getStaticProps를 설정하지 않음. 기본 값으로 설정된 SSG를 의미한다.
=> next.js 에서는 아무것도 설정하지 않은 페이지들을 기본값으로 정적인 페이지로 빌드타임에 미리 사전 렌더링한다.

코드

✨ 결론

SSG는 빌드 타임에 페이지를 미리 사전 렌더링해 둠으로써, 사용자 요청 시 빠르게 응답할 수 있는 렌더링 방식이다.

  • 장점은 사전 렌더링이 오래걸려도, 실제 접속 요청엔 빠르게 응답 가능하다는 것
  • 단점은 매번 똑같은 페이지만 응답하기 때문에, 최신 데이터 반영은 어렵다.
  • Next.js에서는 기본적으로 next.js는 SSG로 사전 렌더링한다.

적용 방법

1) 데이터 페칭이 필요한 경우 getStaticProps 함수를 사용하면 된다.
2) 동적 경로에서, SSG를 적용하고 싶은 경우 getStaticPath 함수를 사용해서, 가능한 모든 경로에 대한 정적 페이지를 생성할 수 있다.
3) 만약 대응하지 못한 동적 경로가 있다면, fallback 옵션 값을 어떻게 하느냐에 따라 다르게 대응할 수 있다.

  • fallback: false : 404 not found를 반환함
  • fallback: "blocking" : SSR 방식으로 페이지 생성
  • fallback: true : SSR 방식 + 데이터가 없는 폴백 상태의 페이지부터 반환

레퍼런스

profile
𝙒𝙝𝙚𝙧𝙚 𝙩𝙝𝙚𝙧𝙚 𝙞𝙨 𝙖 𝙬𝙞𝙡𝙡 𝙩𝙝𝙚𝙧𝙚 𝙞𝙨 𝙖 𝙬𝙖𝙮 ✨

0개의 댓글