oungo

Next.js SSG, SSR, ISR

코로나 격리 기간에 할 게 없어서 간만에 Next.js 문서 정독을 했다. 모든 기능을 설명하기에는 글이 너무 길어질 것 같고, Next.js에서 가장 핵심이라고 할 수 있는 SSG, SSR, ISR에 대해 정리해보려고 한다.

Next.js

Next.js는 리액트 프레임워크다. 서버 사이드 렌더링이나 다이나믹 라우팅 등 리액트 어플리케이션을 개발할 때 필요한 기능들을 쉽게 사용할 수 있도록 도와주는 역할을 한다. 이는 Next.js에서 요구하는 조건에 맞춰서 코드를 작성하기만 하면 된다. 서버 사이드 렌더링과 클라이언트 사이드 렌더링의 차이는 아래 이미지로 확인할 수 있다.

ssr csr 출처 The Benefits of Server Side Rendering Over Client Side Rendering

Pre-rendering

Next.js에서 가장 중요한 컨셉 중 하나가 Pre-rendering이다. 각 페이지의 HTML을 미리 만들어 놓는다는 뜻이다. pre-rendeing이 발생하는지는 간단한 테스트를 통해 확인해 볼 수 있다.

  1. 먼저 npx create-next-app nextjs-example 명령어를 통해 Next.js 기본 프로젝트를 만들어주고, 실행한다.

  2. 크롬 개발자 모드를 켜고, command + shift + p를 눌러서 disable javascript를 누르고 새로고침을 한다. 그러면 자바스크립트를 실행하지 않아도 미리 만들어둔 Html을 보여줌으로써 어플리케이션의 ui를 확인할 수 있습니다.

  3. 똑같은 방식으로 create-react-app 프로젝트를 만들어서 비교해 보면 둘의 차이를 알 수 있다.

Static Generation, Server-side Rendering

Pre-rendering은 크게 두 가지 방식으로 나뉠 수 있는데 Static Generation과 Server-side Rendering이다. 공식 문서에 아래와 같이 설명하고 있다.

Static Generation is the pre-rendering method that generates the HTML at build time. The pre-rendered HTML is then reused on each request.

Server-side Rendering is the pre-rendering method that generates the HTML on each request.

다시 설명하면 Static Generation은 빌드 할 때 HTML을 생성해놓고, 요청이 들어오면 빌드 할 때 만들어둔 HTML을 보여준다. Server-side Rendering은 매 요청 시 HTML을 생성해서 보여준다.

getStaticProps

Next.js에서는 Static Generation 사용을 권장한다. 사용자의 요청과는 관계없이 보여줄 수 있는 페이지라면 Static Generation을 사용하면 된다. Static Generation 사용 방식은 간단하다.

아래와 같이 getStaticProps을 작성하면 Next.js는 빌드 할 때 getStaticProps로부터 리턴된 props를 사용하여 해당 페이지를 pre-render 한다.

export default function Home(props) { ... }

export async function getStaticProps() {
  const data = { name: 'haha' };

  return {
    props: {
      data
    }
  }
}

getStaticProps는 서버사이드에서만 실행된다. javascript bundle에도 포함되지 않는다. 따라서 브라우저를 통하지 않고, getStaticProps 내에서 바로 쿼리를 호출하여 데이터를 가져올 수 있다.

getServerSideProps

Server-side Rendering의 사용법도 비슷하다. getStaticProps 대신 getServerSideProps를 사용하면 된다.

아래와 같이 getServerSideProps를 작성하면 매 요청 시마다 getServerSideProps에서 리턴된 데이터를 가지고 해당 페이지를 pre-render 한다. getServerSideProps 또한 서버사이드에서만 실행된다.

getServerSideProps에서 받고 있는 context에는 다양한 데이터가 담겨있는데 자세한 건 API 문서를 살펴보자.

export async function getServerSideProps(context) {
  const data = { name: 'haha' };

  return {
    props: {
      data,
    },
  };
}

Incremental Static Regeneration(ISR)

Static Generation은 빌드 할 때 HTML을 만들어 두기 때문에 보통 정적인 데이터를 보여주고자 할 때 사용된다. 하지만 Incremental Static Regeneration을 사용하면 빌드 후에도 변경된 데이터를 가져올 수 있다. getStaticProps에 revalidate prop을 추가하면 된다.

import styles from '../styles/Home.module.css';
import axios from 'axios';

export default function Home({ data }) {
  return (
    <div className={styles.container}>
      <h1>{data}</h1>
    </div>
  );
}

export async function getStaticProps() {
  // 현재 시간을 가져오는 API
  const res = await axios.get('https://worldtimeapi.org/api/ip');
  return {
    props: {
      data: res.data.datetime,
    },
  };
}

위와 같은 코드를 작성하고 빌드 후 실행시키면 시간을 화면에 보여주는데 페이지를 새로고침해도 처음 빌드할 때 getStaticProps를 통해 데이터를 가져왔기 때문에 시간은 바뀌지 않는다.

이번엔 revalidate prop을 추가해 보자.

export async function getStaticProps() {
  const res = await axios.get('https://worldtimeapi.org/api/ip');

  return {
    props: {
      data: res.data.datetime,
    },
    revalidate: 60,
  };
}

revalidate를 추가하면 Next.js는 요청이 들어왔을 때, 최대 10초에 한 번 페이지 regeneration을 수행할 것이다.

revalidate를 추가했을 때 동작 방식은 다음과 같다.

  1. 페이지 요청이 들어오면 캐시 된 페이지를 보여준다.

  2. 최초 페이지 요청 후 60초 내로 들어오는 요청들도 캐시 된 페이지를 보여준다.

  3. 10 초 후 요청이 들어왔을 때도 저장된 페이지를 보여주고, Next.js는 백그라운드에서 페이지 regeneration을 수행한다.

  4. 페이지 regeneration이 성공적으로 됐을 경우 이전의 캐시를 없애고 새로운 페이지를 보여준다.

ISR 출처 Incremental Static Regeneration

이처럼 getStaticProps에 revalidate를 사용하면 데이터가 변경되더라도 계속해서 새로운 데이터를 보여줄 수 있다.

On-Demand Revalidation (Beta)

ISR를 통해 SSG에서 정적인 데이터만 보여준다는 단점을 보완할 수 있다. 하지만 revalidate를 설정하면 특정 시간 내에는 무조건 동일한 데이터를 가진 페이지를 볼 수밖에 없다. 그 사이에 데이터가 변경된다고 하더라도 새로운 데이터를 확인할 수 있는 유일한 방법은 revalidate에 설정한 시간이 지난 후 누군가 페이지를 요청을 보내는 것뿐이다.

하지만 Next.js v12.1.0 부터는 On-Demand Revalidation을 지원한다. 원하는 시점에 캐시 된 페이지를 지우고 페이지 regeneration이 가능하다.

사용 방법은 기존에 있던 revalidate를 삭제하고, unstable_revalidate를 사용하면 된다. 먼저 pages/api/revalidate.js 파일에 아래와 같은 코드를 넣어준다.

unstable_revalidate에는 revalidate할 페이지를 넣어준다. 여기서는 인덱스 페이지에 적용할 것이기 때문에 '/'를 넣어줬다

// pages/api/revalidate.js

export default async function handler(req, res) {
  // Check for secret to confirm this is a valid request
  if (req.query.secret !== process.env.MY_SECRET_TOKEN) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    await res.unstable_revalidate('/');
    return res.json({ revalidated: true });
  } catch (err) {
    // If there was an error, Next.js will continue
    // to show the last successfully generated page
    return res.status(500).send('Error revalidating');
  }
}

그리고 Home 컴포넌트에서는 revalidate를 수행할 버튼을 만들고 위에서 만든 걸 호출하면 된다.

import styles from '../styles/Home.module.css';
import axios from 'axios';

export default function Home({ data }) {
  function revalidate() {
    fetch('/api/revalidate');
  }

  return (
    <div className={styles.container}>
      <h1>{data}</h1>
      <button onClick={() => revalidate()}>Revalidate</button>
    </div>
  );
}

export async function getStaticProps() {
  const res = await axios.get('https://worldtimeapi.org/api/ip');
  return {
    props: {
      data: res.data.datetime,
    },
    // revalidate: 5
  };
}

그럼 아래와 같이 동작한다. 버튼을 누르지 않고 새로고침을 하면 계속 동일한 시간이 나온다. revalidate 버튼을 누르고, 다시 새로 고침을 누르면 변경된 시간을 확인할 수 있다.

ISR

마무리

Next.js에서 가장 핵심이라고 할 수 있는 SSG, SSR, ISR 그리고 가장 최근에 나온 On-Demand Revalidation (Beta)까지 알아보았다. 참고로 전부 page 내에서만 사용이 가능하고, 페이지별로 다르게 적용할 수 있다.

예전에 처음 Next.js로 프로젝트를 시작했을 때는 getInitialProps밖에 없었던 걸로 기억하는데 그동안 많이 바뀐 것 같다. Next.js를 사용해 보지 않았다면 한번쯤 사용해 보는 것을 추천한다. 사용법도 간단하고 문서도 굉장히 잘 되어있기 때문에 초보자가 접근하기에도 무리가 없을 거라고 생각한다.