[Next.JS] SSG - getStaticPaths

2023. 2. 21. 20:28·Next.JS
반응형

NextJS 로고


getStaticProps에 대해 익숙하지 않다면, 아래 글을 먼저 보는 것을 추천 드립니다.

 

[Next.JS] SSG - getStaticProps

getStaticProps SSG(Static Site Generator)는 페이지를 호출할 때마다 서버에서 pre-render하는 SSR와 달리 빌드시점에 pre-rendering을 한다. // posts는 빌드 시점에 getStaticProps()에 의해 채워진다. function Blog({ posts }

soojae.tistory.com

 

getStaticPaths

동적 경로를 사용하는 페이지에서 getStaticPaths라는 함수를 사용할 때 Next.js는 getStaticPaths에 지정된 모든 경로를 정적으로 미리 렌더링한다.

paths

paths값은 pre-rendering할 경로를 결정한다. 예를 들어 pages/posts/[id].js라는 이름의 동적 경로를 사용하는 페이지가 있다고 가정해보자. 이 페이지에서 getStaticPaths 메서드를 통해 다음을 return한다면

export async function getStaticPaths() {
    return {
      paths: [
        { params: { id: '1' }},
        {
          params: { id: '2' },
          // i18n을 구성하면 경로에 대한 locale도 return할 수 있다.
          locale: "en",
        },
      ],
      fallback: true, false 또는 "blocking" 
    }
}

위와 같이 작성하면 /posts/1 와 /posts/2이 `next build`시에 정적으로 페이지가 생성된다.

  • 페이지 이름이 `pages/posts/[postId]/[commentId]`인 경우 params에는 `postId`와 `commentId`가 포함되어야 한다.
  • 페이지 이름이 `pages/[...slug]`와 같은 포괄 경로를 사용하는 경우 매개변수에는 배열인 slug가 포함되어야 한다. 이 배열이 `['hello', 'world']`인 경우 Next.js는 /hello/world에 페이지를 정적으로 생성한다.
  • 페이지에서 optional catch-all-route를 사용 할때, `null, [], undefined, false`를 사용하여 루트 최상위 경로를 렌더링한다. 예를 들어, `pages/[[...slug]]`에 `slug: false`를 제공하면 Next.js는 `/` 페이지를 정적으로 생성한다.
  • params는 case-sensitive하다. 그래서 Posts와 posts는 다른 경로로 인식한다.

 

Fallback 옵션

false일 경우

  • getStaticPaths에서 return하지 않은 path는 404 페이지를 return한다.
  • 이 옵션은 생성할 경로 수가 적거나 새 페이지 데이터를 자주 추가하지 않는 경우에 유용하다
  • 경로를 더 추가해야 하는데 `fallback: false`로 설정한 경우 빌드를 다시 실행해야 한다.

 

true일 경우

getStaticProps의 동작은 다음과 같은 방식으로 변경된다.

  • getStaticPaths에서 return된 경로는 빌드 시 getStaticProps에 의해 HTML로 렌더링된다.
  • 빌드 시점에 생성되지 않은 경로는 404 페이지가 생성되지 않는다. 대신 Next.js는 해당 경로에 대한 첫 번째 요청 시 페이지의 "fallback" 버전을 제공한다. 구글과 같은 웹 크롤러는 fallback이 제공되지 않고, `path`는 `fallback: blocking`처럼 동작한다.
  • `fallback: true`로 설정된 페이지가 `next/link` 또는 `next/router`(클라이언트 측)를 통해 탐색되는 경우 Next.js는 fallback을 제공하지 않고 대신 페이지가 `fallback: blocking`으로 동작한다.
  • 백그라운드에서 Next.js는 요청된 경로 HTML과 JSON을 정적으로 생성한다. 이때 getStaticProps도 실행된다.
  • 완료되면 브라우저는 생성된 경로에 대한 JSON을 수신한다. 이는 필요한 props들이 포함된 페이지를 자동으로 렌더링하는 데 사용된다. 사용자 입장에서는 페이지가 fallback 페이지에서 full 페이지로 바뀐다.
  • 동시에 Next.js는 이 경로를 미리 렌더링된 페이지 목록에 추가하여 동일한 경로에 요청이 들어오면, 빌드 할 때 pre-rendering된 페이지들과 마찬가지로 동작한다.
`fallback: true`는 next export를 사용할땐 동작하지 않는다.

 

fallback: true는 언제 사용해야 할까?

  • 앱 데이터에 의존하는 정적페이지가 매우 많은 경우(ex: 매우 큰 전자 상거래 사이트)에 유용하다. 모든 제품 페이지를 빌드 시간에 미리 렌더링 하는 것은 매우 오랜 시간이 걸린다.
  • 대신 페이지의 작은 하위 subset을 정적으로 생성하고 나머지 페이지에 대해서는 `fallback:true`로 한다. 사용자가 아직 생성되지 않은 페이지를 요청하면 loading indicator 또는 skeleton 컴포넌트 페이지가 표시된다.
  • getStaticProps가 완료되면 요청된 데이터로 페이지가 렌더링된다.
  • `fallback: true`일 경우 생성된 페이지를 업데이트 할 수 없다. ISR을 확인하자

 

Blocking일 경우

true일 경우와 비슷하게 동작하지만, getStaticPaths가 return하지 않은 새 path는 SSR과 동일하게 HTML이 생성될 때까지 기다린 다음 이후 요청을 위해 캐시되어 `path`당 한 번만 발생한다.

  • getStaticPaths에서 return된 `path`는 빌드 시점에 getStaticProps에 의해 HTML로 렌더링된다.
  • 빌드 시점에 생성되지 않은 경로는 404 페이지가 생성되지 않는다. 대신 Next.js는 첫 번째 요청에 대해 SSR을 수행하고 생성된 HTML을 return한다.
  • 완료되면 브라우저는 생성된 경로에 대한 HTML을 수신한다. 사용자 입장에서는 "브라우저가 페이지를 요청 중"에서 "전체 페이지가 로드됨"으로 전환된다. 로딩/폴백 상태의 플래시가 표시되지 않는다.
  • 동시에 Next.js는 이 경로를 미리 렌더링된 페이지 목록에 추가한다. 동일한 경로에 대한 후속 요청은 빌드 시 미리 렌더링된 다른 페이지와 마찬가지로 생성된 페이지를 제공한다.
  • `fallback: 'blocking'`일 경우 생성된 페이지를 업데이트를 하지 않는다. 업데이트 하려면 ISR도 함께 사용해야한다.
`fallback: true`는 next export를 사용할땐 동작하지 않는다.

 

Fallback 페이지

  • 페이지의 props가 비어있다.
  • `router`를 사용하면 fallback이 렌더링 되고있는지 감지할 수 있고, 렌더링 되는 중이라면 `router.isFallback` 값은 true를 return한다.
// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // 만약 page가 아직 생성되지 않았으면 getStaticProps()가 끝날때까지 
  // `<div>Loading...</div>`이 보여진다.
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // post관련 코드...
}

// build 시점에 이 메서드가 호출된다.
export async function getStaticPaths() {
  return {
    // `/posts/1` 과 `/posts/2`에 대한 페이지만 생성된다.
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // fallback 값이 true이므로 정적으로 추가 페이지 생성할 수 있다.
	// 예: `/posts/3`
    fallback: true,
  }
}

// build 시점에 이 메서드가 호출된다.
export async function getStaticProps({ params }) {
  // params은 id값을 갖고 있다. `id`.
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return {
    props: { post },
    // 요청이 들어올 때, 최대 10초마다 한 번 페이지를 재생성한다.
    revalidate: 10,
  }
}

export default Post

 

Fallback 테스트

※ 테스트 코드는 제 깃허브(https://github.com/SooJae/nextjs-ssg-fallback)에서 확인하실 수 있습니다.

  • getStaticPaths 메서드에서 id가 1~5인 유저 리스트를 조회하여 path를 return한다.
  • getStaticProps 메서드에서 해당 페이지에 접근할 경우 해당 id의 사용자를 조회한다. 조회 전 의도적으로 getStaticProps의 실행을 늦추기 위해 sleep 시간을 1초로 지정한다. (Fallback이 true일때와 'blocking'일때의 비교를 하기위해)
  • http://localhost:3000/users/{id} 경로로 접근

이때 각 Fallback값에 따라 결과가 어떻게 바뀌는지 확인해보자.

`$ yarn dev` 실행시 페이지에 매번 접근할 때마다 getStaticProps가 호출되므로 정확한 테스트를 위해,
`$ yarn build && yarn start`로 실행하여 배포할 때와 똑같은 환경으로 테스트 하자

 

Fallback: false인 경우

// pages/users/[id].tsx

import {GetStaticPropsContext} from "next";
import axios from "axios";

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

export const UserDetailPage = ({user}: UserDetailPageProps) => {
    return (
        <div>
            {user.id} / {user.name} / {user.email}
        </div>
    )
}

export const getStaticPaths = async () => {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user => ({params: {id: user.id.toString()}}));

    return {
        paths,
        fallback: false,
        // fallback: true,
        // fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) => {
    const id = context.params?.id || '';

    await sleep(1000); // 사실 fallback: false일 경우에는 영향이 없다.

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

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms))
}


export default UserDetailPage;

 

결과

Next.js getStaticPaths fallback:false 결과

  • 존재하지 않는 path({id: 7}) 페이지에 접근하면 404 에러 페이지가 나온다.

 

Fallback: true인 경우

// pages/users/[id].tsx

import {GetStaticPropsContext} from "next";
import axios from "axios";
import {useRouter} from "next/router";

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

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

export const getStaticPaths = async () => {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user => ({params: {id: user.id.toString()}}));

    return {
        paths,
        // fallback: false,
        fallback: true,
        // fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) => {
    const id = context.params?.id || '';

    await sleep(1000);

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

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms))
}


export default UserDetailPage;

 

결과

Next.js getStaticPaths fallback:true 결과

  • 존재하지 않는 `path`({id: 7}) 페이지에 접근하면 `Loading...`(sleep 1초)이 보이고 페이지가 생성된다.
  • 이때 다른 페이지({id:1})로 이동했다가 다시 {id:7} 페이지로 접근하면 로딩없이 바로 나온다. 즉 페이지가 캐싱되었다는 것을 알 수 있다.
  • 주의할 점은 `fallback:false` 때와 달리 위의 코드에서 19~27번 라인의 코드가 없으면, 아래와 같은 에러가 발생하고, 빌드에 실패한다. (https://nextjs.org/docs/messages/prerender-error)

Next.js getStaticPaths fallback:true 에러

 

Fallback: 'blocking'인 경우

// pages/users/[id].tsx

import {GetStaticPropsContext} from "next";
import axios from "axios";
import {useRouter} from "next/router";

interface User {
    id: number,
    name: string,
    username: string,
    email: string
}

interface UserDetailPageProps {
    user: User
}

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

export const getStaticPaths = async () => {

    const {data: users}: { data: User[] } = await axios.get('https://jsonplaceholder.typicode.com/users');
    // 유저 리스트에서 5명.
    const partialUserList = users.slice(0, 5);
    const paths = partialUserList.map(user => ({params: {id: user.id.toString()}}));

    return {
        paths,
        // fallback: false,
        // fallback: true,
        fallback: 'blocking',
    }
}

export const getStaticProps = async (context: GetStaticPropsContext) => {
    const id = context.params?.id || '';

    await sleep(1000);

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

    return {
        props: {
            user
        }
    }
}

const sleep = (ms: number) => {
    return new Promise(resolve => setTimeout(resolve, ms))
}


export default UserDetailPage;

 

결과

Next.js getStaticPaths fallback:'blocking' 결과
탭의 로고를 보면 로딩 중인것을 확인할 수 있다.

  • Fallback: true일때와 비슷하지만, 페이지 생성 중에 fallback 버전의 페이지(`
    Loading...
    `)를 보여주지 않는다.
  • loading 상태를 확실히 보여주기 위해 sleep을 1초가 아닌 2초로 두었다. 즉 로딩시간 1초 이내면 사용자가 로딩 중인것도 인지하기 힘들다.

 

Fallback 값 true vs 'blocking'

  • 시간이 짧은 로직일 경우 - true일 경우 화면이 loading 상태 -> 렌더링이 완료된 페이지로 바뀌는 찰나의 flickering(혹은 flashing)때문에 UX가 오히려 안 좋아진다. 이때는 blocking을 사용하는 것이 좋아 보인다.
  • 시간이 긴 로직일 경우 - bloking일 경우 화면의 변화가 없기 때문에, 사용자 입장에서는 웹사이트에 에러가 발생한 것으로 인식될 수 있다. 그러니 오래걸리는 로직은 true로 두어 loading 상태를 보여주는 것이 좋아보인다.

 


 

REFERENCES

Next.js 공식 사이트 - https://nextjs.org/docs/basic-features/data-fetching/get-static-props#runs-on-every-request-in-development
mskwon님 블로그 - https://velog.io/@mskwon/next-js-static-generation-fallback

반응형
저작자표시 동일조건
'Next.JS' 카테고리의 다른 글
  • [Next.JS] SSG - getStaticProps
  • [Next.JS] SSR - getInitialProps, getServerSideProps
  • [Next.JS] _app.js, _document.js
SooJae
SooJae
코드는 효율적으로, 공부는 비효율적으로
    반응형
  • SooJae
    이수재 블로그
    SooJae
  • 전체
    오늘
    어제
    • 분류 전체보기 (60)
      • Spring (8)
      • Next.JS (4)
      • React (3)
      • Angular (1)
      • Language (6)
        • Java (1)
        • Kotlin (1)
        • Javascript (4)
      • Keycloak (5)
      • Knowledge (16)
        • Test (4)
        • Web (9)
        • Security (2)
        • Data Structure (1)
      • Infra (9)
        • Proxmox (2)
        • AWS (0)
        • Kubernetes (3)
      • Tools (1)
        • IntelliJ (1)
      • Algorithm (2)
      • Tistory (4)
      • ETC (1)
  • 블로그 메뉴

    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    티스토리챌린지
    deepl api
    spring ai
    GPT
    Functional Programming
    Kotlin
    Auth
    test
    javascript
    springboot
    ChatGPT
    openAI
    오블완
    React
    keycloak
    웹 마스터 도구
    스프링 ai
    Next.js
    ai
    스프링 번역
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.0
SooJae
[Next.JS] SSG - getStaticPaths
상단으로

티스토리툴바