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