Next JS Static Generation - fallback

Min Su Kwon·2021년 7월 3일
29
post-thumbnail

Next JS에서 파라미터가 동적으로 바뀌는 페이지(ex - users/:userId)를 빌드타임에 생성하려면, 해당 페이지 컴포넌트에서 getStaticPaths + getStaticProps 함수를 export 해줘야한다.

getStaticPaths 반환 객체

이 때, 어떤 path들에 대해서 정적 페이지 생성을 할지 정하는 getStaticPaths 함수는 반환 객체로 paths 키와 fallback 키를 반드시 포함시켜야한다.

pages/users/[id].js에서 getStaticPaths 함수가 아래와 같은 객체를 반환하면,

// getStaticPaths
return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

Next JS는 users/1, users/2 페이지를 빌드타임에 생성하게 된다.

paths

어떤 path의 페이지들을 빌드 타임에 생성할지 정하는 배열이다.

paths 배열에서 각각의 params 값은 페이지 이름에 있는 파라미터와 일치해야한다.

여기서 주의할 점들은 다음과 같다.

  • 만약 파일명이 pages/users/[userId].js 였다면, params 객체도 userId 키값을 가지고 있어야한다.
  • 파일명이 pages/[...slug]와 같이 catch-all 라우트를 이용중인 경우, params 객체는 slug 배열을 가지고 있어야한다.
  • 파일명이 pages/[[..slug]]와 같이 optional catch-all 라우트를 이용중인 경우, null / [] / undefined / false 값을 넣어주면 루트 라우트를 렌더링하게 된다.

fallback

빌드 타임에 생성해놓지 않은 path로 요청이 들어온 경우 어떻게 할지 정하는 boolean 또는 'blocking' 값이다.

  • false인 경우

    getStaticPaths가 반환하지 않은 모든 path에 대해서 404 페이지를 반환한다.

    아래와 같은 경우에 사용할 수 있다

    • 적은 숫자의 path만 프리렌더링 해야하는 경우
    • 새로운 페이지가 추가 될 일이 많지 않은 경우
      → 새로운 페이지가 자주 추가 된다면, 추가될때마다 다시 빌드해줘야한다
  • true인 경우

    getStaticProps의 동작이 바뀌게 된다.

    1. getStaticPaths가 반환한 path들은 빌드 타임에 HTML로 렌더링된다

    2. 이외의 path들에 대한 요청이 들어온 경우, 404 페이지를 반환하지 않고, 페이지의 "fallback" 버전을 먼저 보여준다

    3. 백그라운드에서 Next js가 요청된 path에 대해서 getStaticProps 함수를 이용하여 HTML 파일과 JSON 파일을 만들어낸다

    4. 백그라운드 작업이 끝나면, 요청된 path에 해당하는 JSON 파일을 받아서 새롭게 페이지를 렌더링한다. 사용자 입장에서는 [ fallback → 풀 페이지 ]와 같은 순서로 화면이 변하게된다.

    5. 새롭게 생성된 페이지를 기존의 빌드시 프리렌더링 된 페이지 리스트에 추가한다. 같은 path로 온 이후 요청들에 대해서는 이때 생성한 페이지를 반환하게된다.

      "fallback" 상태일 때 보여줄 화면은 next/routerrouter.isFallback 값 체크를 통해서 조건 분기하면 된다. 이때 페이지 컴포넌트는 props로 빈값을 받게된다.

      // pages/posts/[id].js
      import { useRouter } from 'next/router'
      
      function Post({ post }) {
        const router = useRouter()
      
        // If the page is not yet generated, this will be displayed
        // initially until getStaticProps() finishes running
        if (router.isFallback) {
          return <div>Loading...</div>
        }
      
        // Render post...
      }

      아래와 같은 경우에 사용할 수 있다

    • 데이터에 의존하는 정적 페이지를 많이 가지고 있으나, 빌드 시에 모든 페이지를 생성하는건 너무나 큰 작업일 때
      • 몇몇 페이지들만 정적으로 생성하게 하고, fallback 옵션을 true로 설정해주면 이후 요청이 오는 것에 따라서 정적 페이지들을 추가하게 된다
        → 빌드 시간도 단축하고, 대부분 사용자들의 응답 속도도 단축할 수 있다
  • 'blocking'일 경우

    true일 경우와 비슷하게 동작하지만, 최초 만들어놓지않은 path에 대한 요청이 들어온 경우 fallback 상태를 보여주지 않고 SSR처럼 동작한다. 이후 true 옵션과 같이 기존의 정적 페이지 리스트에 새로 생성한 페이지를 추가한다.

예시

fallback: false

import axios from 'axios'
import _ from 'lodash';

export default function UserDetailPage({ user }) {
  return (
    <div>
      {user.id} / {user.name} / {user.email}
    </div>
  )
}

export async function getStaticPaths() {
  const { data: users } = await axios.get('https://jsonplaceholder.typicode.com/users');
  const paths = _.map(_.slice(users, 0, 5), (user) => {
    return { params: {id: _.toString(user.id)}};
  });

  return {
    paths,
    fallback: false
  }
}

export async function getStaticProps(context) {
  const { id } = context.params;

  const { data: user } = await axios.get(`https://jsonplaceholder.typicode.com/users/${id}`);

  return {
    props: {
      user
    }
  }
}

fallbackfalse인 경우에는, 사전에 빌드하지 않은 path에 대해서 404 응답을 반환한다.

위 예시에서는 1~5번 까지의 id를 가지는 path들에 대해서만 앱 빌드시 페이지 생성을 하고 있다. 따라서, 다른 id의 요청이 들어올 경우, 404 응답을 반환한다.

fallback: true

import axios from 'axios'
import { useRouter } from 'next/router';
import _ from 'lodash';

export default function UserDetailPage({ user }) {
  const router = useRouter();
	// fallback version
  if (router.isFallback) {
    return (
      <div>
        Loading...
      </div>
    )
  }
  return (
    <div>
      {user.id} / {user.name} / {user.email}
    </div>
  )
}

export async function getStaticPaths() {
  const { data: users } = await axios.get('https://jsonplaceholder.typicode.com/users');
  const paths = _.map(_.slice(users, 0, 5), (user) => {
    return { params: {id: _.toString(user.id)}};
  });

  return {
    paths,
    fallback: true
  }
}

export async function getStaticProps(context) {
  const { id } = context.params;

  const [{ data: user }] = await Promise.all([
    axios.get(`https://jsonplaceholder.typicode.com/users/${id}`),
    timeout(5000)
  ]);

  return {
    props: {
      user
    }
  }
}

function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

fallbacktrue인 경우에는 사전에 빌드하지 않은 path에 대해서 요청이 들어올시 첫 요청 시기에 getStaticProps 호출을 통해 페이지를 만들고, 빌드된 path 리스트에 추가한다. 페이지가 아직 만들어지지 않았을 때, 페이지의 fallback 버전을 렌더링한다.

위 코드 예시에서는 의도적으로 getStaticProps의 실행을 늦추기 위해 timeout 함수를 선언해서 사용했다. getStaticProps의 실행에는 최소 5초가 걸리게된다.

즉, 새로운 path로의 첫 요청을 하는 사용자는 최소 5초동안 fallback 페이지를 보게된다.

빌드 후에 파일을 확인해보면, [id].html이 추가로 생성되는데, 이 파일이 /users/[id] 라우트로의 fallback 버전이 된다. 실제로 파일을 열어보면 아래와 같이 페이지 컴포넌트에서 정의한 fallback 버전을 볼 수 있다. 참고로, fallback 버전이 지정되지 않으면 빌드과정에서 오류가 발생한다.

그리고 사전에 빌드되지 않은 path에 대한 요청이 들어가게 되면,

위와 같이 페이지 생성이 완료되기 전까지 fallback 버전을 보여주다가, 생성이 완료되면 생성된 페이지를 저장하고, 보여주게 된다.

이렇게 새로운 path로의 첫 요청은 오래 걸리지만, 이때 페이지를 생성하고 저장해놓기 때문에, 이후 요청들에 대해서는 다른 빌드시 생성된 페이지들과 마찬가지로 사용자가 매우 빠르게 페이지를 볼 수 있게 된다.

fallback: 'blocking'

import axios from 'axios'
import _ from 'lodash';

export default function UserDetailPage({ user }) {
  return (
    <div>
      {user.id} / {user.name} / {user.email}
    </div>
  )
}

export async function getStaticPaths() {
  const { data: users } = await axios.get('https://jsonplaceholder.typicode.com/users');
  const paths = _.map(_.slice(users, 0, 5), (user) => {
    return { params: {id: _.toString(user.id)}};
  });

  return {
    paths,
    fallback: 'blocking'
  }
}

export async function getStaticProps(context) {
  const { id } = context.params;

  const [{ data: user }] = await Promise.all([
    axios.get(`https://jsonplaceholder.typicode.com/users/${id}`),
    timeout(5000)
  ]);

  return {
    props: {
      user
    }
  }
}

function timeout(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

fallback'blocking'인 경우 true일때와 비슷하지만, 페이지 생성중에 사용자에게 fallback 버전의 페이지를 보여주지 않고, ssr처럼 동작해서 아무것도 미리 보여주지 않게된다.

위 코드 예시에서 볼 수 있듯이 fallback 상태에 따라서 뭘 보여줄지 정의하지 않아도 되고, 빌드된 파일을 살펴보면 fallback: true 처럼 [id].html을 만들어놓지도 않는다.

마찬가지로 getStaticProps에 5초의 타임아웃을 걸어놓았기 때문에, 사용자는 빌드되지 않은 path로의 요청을 하는 경우 해당 시간동안 브라우저의 로딩 상태를 보게된다.

마찬가지로, 이후 같은 path로의 요청에는 빠른 응답을 할 수 있게 된다.

Reference

profile
이제 막 커리어를 시작한 소프트웨어 엔지니어입니다. 배운 것을 정리하면서 조금 더 깊이 이해하려는 습관을 들이려고 합니다. 피드백은 언제나 환영입니다.

5개의 댓글

comment-user-thumbnail
2021년 12월 3일

와.......진짜 도움 많이 됐습니다
진심으로 감사합니다

답글 달기
comment-user-thumbnail
2022년 1월 30일

예시로 보여주신부분 너무 좋네요. 잘봤습니다!

답글 달기
comment-user-thumbnail
2022년 6월 20일

포스팅이 엄청 깔끔하네요.
도움이 많이 되었습니다 👍

답글 달기
comment-user-thumbnail
2023년 1월 15일

감사합니다

답글 달기
comment-user-thumbnail
2023년 2월 1일

좋은 글 감사합니다! 도움이 많이 됐어요

답글 달기