티스토리 뷰
[Next.js] SSG를 활용해서 다이나믹한 page 생성 with getStaticPaths, fallback 에러 해결
doeunnkimm 2023. 6. 25. 19:40지난 포스팅에서 md 파일들에 있는 데이터를 읽어와서 SSG를 통해 렌더링을 했었습니다.
만약! 추가로 다음과 같은 요구사항이 있다면 어떻게 해야할까요?
- SSG를 사용해서
- url: /posts/ssg-ssr 이면 posts/ssg-ssr.md를 읽어서 보여준다
- url: /posts/pre-rendering 이면 posts/pre-rendering.md를 읽어서 보여준다
→ /pages/posts/[slug].js 가 떠올랐어야 합니다.
post의 제목들 자리에는 dynamic하게 들어가야 하기 때문에 slug를 활용해야겠습니다.
SSG를 활용해서 다이나믹한 page 들을 생성하려면?
getStaticPaths가 필요합니다. getStaticPaths는 생성해둬야 하는 페이지 정보를 배열로 반환합니다.
요구사항을 해결해보자
우선 우리가 getStaticPaths에 생성해둬야 하는 페이지 정보를 배열로, 즉, post들의 id를 배열 안에서 관리한다는 말입니다. 그러면 post들의 id를 추출하는 작업도 필요해 보입니다. 이를 함수로 만들어볼까요?
📜 lib/posts.js
export function getAllPostIds() {
const fileNames = fs.readdirSync(postsDiretory)
// 아래 배열과 같은 형태를 리턴한다.
// [
// { params: { id: 'ssg-ssr' },
// { params: { id: 'pre-rendering' }
// ]
return fileNames.map((fileName) => {
return {
params: {
id: fileName.reaplce(/\.md$/, ''),
},
}
})
}
그럼 위 함수를 호출하여 변수에 담아주면 모든 post의 id를 담은 배열이 담아지겠습니다.
그다음은, 이 id가 담긴 배열을 getStaticPaths에서 받을 차례입니다.
📜 pages/posts/[id].js
import { getAllPostIds } from '../../lib/posts'
export const function getStaticPaths() {
const paths = getAllPosts()
return {
paths,
fallback: false
}
}
getStaticPaths가 반환하는 fallback ?
fallback은 빌드 타임에 생성해놓지 않은 path로 요청이 들어온 경우 어떻게 할지 정하는 `boolean' | 'blocking' 값입니다.
- false인 경우
getStaticPaths가 반환하지 않은 모든 path에 대해서 404 페이지를 반환
아래와 같은 경우에 사용할 수 있습니다.
- 적은 숫자의 path만 프리렌더링 해야하는 경우
- 새로운 페이지가 추가될 일이 많지 않은 경우 → 새로운 페이지가 자주 추가된다면 추가될 때마다 다시 빌드필요
- true인 경우
getStaticProps의 동작이 바뀌게 됩니다.- getStaticPaths가 반환한 path들은 빌드 타임에 HTML로 렌더링
- 이외의 path들에 대한 요청이 들어온 경우, 404 페이지를 반환하지 않고, 페이지의 fallback 버전을 먼저 보여준다
- 백그라운드에서 Next.js가 요청된 path에 대해서 getStaticProps 함수를 이용하여 HTML파일과 JSON 파일을 만들어낸다
- 백그라운드 작업이 끝나면, 요청된 path에 해당하는 JSON파일을 받아서 새롭게 페이지를 렌더링한다. 사용자 입장에서는 [ fallback → 풀 페이지 ]와 같은 순서로 화면이 변하게 된다
- 새롭게 생성된 페이지를 기존의 빌드시 프리렌더링된 페이지 리스트에 추가한다
같은 path로 온 이후 요청들에 대해서는 이때 생성한 페이지를 반환하게 된다
"fallback" 상태일 때 보여줄 화면은 `next/router`의 `router/isFallback` 값 체크를 통해서 조건 분기하면 된다. 이때 페이지 컴포넌트는 props로 빈값을 받게된다.
데이터에 의존하는 정적 페이지를 많이 가지고 있으나, 빌드 시에 모든 페이지를 생성하는 건 너무나 큰 작업일 때
몇몇 페이지들만 정적으로 생성하게 하고, fallback 옵션을 true로 설정해주면 이후 요청이 오는 것에 따라서 정적 페이지들을 추가하게 된다.
→ 빌드 시간도 단축, 대부분 사용자들의 응답 속도도 단축
import { useRouter } from 'next/router'
export function Post({ postData }) {
const router = useRouter()
if (router.isFallback) {
return <div>Loading...</div>
}
}
- 'blocking'인 경우
true일 경우와 비슷하게 동작하지만, 최초 만들어놓지 않은 path들에 대한 요청이 들어온 경우 fallback 상태를 보여주지 않고 SSR처럼 동작한다. 이후 true 옵션과 같이 정적 페이지 리스트에 새로 생성한 페이지를 추가한다.
이제 이 id를 가지고 어떤 내용을 불러와야 하는지에 대한 함수를 선언해줘야겠습니다.
📜 lib/posts.js
export function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// meta 데이터를 읽어온다
const matterResult = matter(fileContents)
// id랑 내용이랑 같이 return
return {
id,
...matterResult.data
}
}
예상을 해봅시다. /posts/ssg-ssr 이라는 url로 들어온다면 우리가 slug 자리는 [ id ] 로 했기 때문에 id: ssg-ssr로 들어오면 그걸 통해 getPostData에 id를 넘기고 리턴받은 내용을 가지고 화면을 렌더링하면 되겠습니다.
📜 pages/posts/[id].js
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
// console.log({ params }) ex. { params: { id: 'ssg-ssr' } }
return {
props: {
postData,
},
}
}
prop으로 받는 params가 궁금해서 콘솔 명령어를 통해 확인해 보았더니, 위와 같이 id라는 키로 url에 들어온 값을 가지고 객체로 받아지는 것을 확인할 수 있었습니다.
이제 다했습니다! 이걸 이제 컴포넌트가 prop으로 받아 화면에 렌더링 해주기만 하면 끝입니다.
📜 pages/posts/[id].js
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
에러들을 일부러 내면서 고쳐보자
1. getStaticProps를 2번 반복
두 번 사용하게 되면 이미 선언되어있다고 알아서 알려주는 것을 확인할 수 있었습니다.
2. 함수를 import 하지 않거나
사용하고 있는 defined 되지 않았다고 이것 역시도 알아서 알려주는 것을 확인할 수 있었습니다.
3. postData를 page에서 props로 받지 않거나
export default function Post() {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
postData 쓴다면서 prop으로 받지는 않았어 하고 이것도 알아서 알려주는 것을 확인할 수 있습니다.
.md 파일을 해석해보자
.md 파일을 해석하기 위해서는 도구를 설치해야 합니다.
$ yarn add remark remark-html
우선 아까 선언했던 `getPostData` 함수를 수정해줄 건데요. 이 가져온 content를 html로 변환하여 return 해주기만 하면 됩니다.
📜 lib/posts.js
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// mate 데이터를 읽어온다
const matterResult = matter(fileContents)
// html로 변환
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
// id랑 내용이랑 같이 return
return {
id,
contentHtml,
...matterResult.data,
}
}
주의해야 하는 점은 `remark()` 앞에 await가 붙기 때문에 함수 자체에도 async를 붙여줘야 합니다. 또한 해당 `getPostData` 함수를 호출하는 부분에서도 await 키워드를 사용해야만 합니다.
📜 pages/posts/[id].js
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData,
},
}
}
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
)
}
추가로, Date formatting 도구까지!
$ yarn add date-fns
날짜 부분은 좀 더 예쁘게 보여주고 싶어서 하는 것 뿐이긴 합니다..!
📜 componets/Dats.js
import { parseISO, format } from 'date-fns'
export default function Date({ dateString }) {
const date = parseISO(dateString)
return <time dateTime={dateString}>{format(date, 'LLLL d, yyyy')}</time>
}
`dateString`이라는 prop에는 '2023-01-01' 와 같은 format의 date가 들어올 것입니다. 그걸 다시 원하는 format으로 해서 리턴해주고 있습니다.
📜 pages/posts/[id].js
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
<Date dateString={postData.date} />
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
)
}
방금 전에 만들었던 Date 컴포넌트를 이용해서 date를 화면에 렌더링합니다.
아까 fallback을 기억하시나요? fallback: true로 했더니 Error ?
getStaticPaths는 빌드 타임에 실행되는 것이기 때문에 `yarn dev`로 했을 때는 정상적으로 동작하지 않는 경우도 있는데요. 따라서 `yarn buid`, `yarn start`를 해서 확인을 한번더 해봅시다.
시도를 해보았더니 `build` 자체가 되지 않습니다.
에러를 확인해 보니 '/' 페이지를 그릴 때 에러가 발생했다고 하네요.
export async function getStaticProps() {
const response = await fetch('http://localhost:3000/api/posts')
const json = await response.json()
return {
props: {
allPostsData: json.allPostsData,
},
}
}
위 코드에서 getStaticProps는 build할 때 화면을 그리는 것인데요. 근데 localhost는 우리가 서버를 띄워야만 떠있는 것이므로 위 형태로는 절대 동작을 할 수가 없습니다. 하지만 getStaticProps 대신 getServerSideProps를 하면 동작을 하게 됩니다.
이제 다시 build를 하고 start를 해보면 다음과 같이 실행되게 됩니다.
잠깐 로딩이 떴다가 Client Side에서 어떠한 에러가 났다는 메세지를 화면에 보여주고 있습니다. 왜냐하면
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: true,
}
}
export async function getStaticProps({ params }) {
const postData = await getPostData(params.id)
return {
props: {
postData,
},
}
}
path를 조회할 때랑 postData를 조회할 때 모두 'aaa'가 없기 때문입니다.
이번에는 fallback: 'blocking'으로 변경하고 build, start를 다시 해봅시다.
이번에는 500에러가 났습니다. 즉, Server Side 에러를 의미합니다.
이를 해결하려면?
이는 build 시 데이터가 없기 때문에 일어나는 에러이기 때문에 아래와 같이 build 타임에 요소가 존재하도록 해줍니다.
xport async function getStaticPaths() {
// const paths = getAllPostIds()
// 아래와 같이 작성한다면 build 타임에 아래와 같은 요소가 존재하게 되는 것
const paths = [
{
params: {
id: 'ssg-ssr',
},
},
]
return {
paths,
fallback: 'blocking',
}
}
fallback을 'blocking'으로 했기 때문에 없어도 화면을 fallback을 그리지 않고 기다렸다가 데이터가 오면 그때 generation하겠다는 것입니다.
fallback을 true로 하게 되면 우리가 build시에도 알고 있도록 했던 ssg-ssr은 pre-rendering 덕분에 바로 화면에 그릴 수 있지만 다른 걸 입력하게 되면 loading이 뜨고 데이터가 불러와지면 그때 화면을 그리게 됩니다.
제가 getAllPostId라는 함수를 주석처리하고 위와 같이 하드로 배열 형식으로 둔 이유는 build시 가지고 있는 데이터에 대해 pre-rendering할 때와 그렇지 않은 데이터에 대해 pre-rendering하지 않아 fallback이 생기는 것에 대한 차이를 보이기 위해였습니다.
기존대로 getAllPostId를 통해 가져온다면, 빌드시에는 아무 것도 가지고 있지 않기 때문에 모든 요소에 대해서 pre-render하지 않겠죠.
오늘은 SSG를 활용하여 Dynamic Routes를 구현하는 것에 대해 알아보았습니다.
1. 하나의 파일로 여러 페이지
Dynamic Routes
2. SSG 시 생성할 목록
getStaticPaths (paths 배열 반환)
3. 도구: md 파싱/date format
remark & remark-html / date-fns
4. getStaticPaths fallback
빌드시 생성되지 않은 page에 대해 어떻게 처리할 것인지
'프론트엔드 > Next.js' 카테고리의 다른 글
[Next.js] Dynamic Import(Lazy load) & Hydration & Static Export (0) | 2023.06.27 |
---|---|
[Next.js] /post/write 페이지에서 새로운 글을 쓸 수 있도록 해보세요 (0) | 2023.06.26 |
[Next.js] SSG 선택 기준과 SSG를 하는 두 가지 방법(with data, without data) (0) | 2023.06.23 |
[Next.js] Layout과 Styling - Image 컴포넌트, Head 컴포넌트, Global CSS (0) | 2023.06.23 |
[Next.js] Next.js가 제공하는 여러 기능들 - Client-side Navigate 부터 Prefetching까지 (0) | 2023.05.29 |
- Total
- Today
- Yesterday
- react
- 데이터분석
- 프론트엔드 공부
- 디프만
- 자바스크립트 기초
- frontend
- JSP
- 프론트엔드 기초
- 자바
- 리액트 훅
- 인프런
- 자바스크립트
- rtl
- 타입스크립트
- 스타일 컴포넌트 styled-components
- 파이썬
- 프로젝트 회고
- Python
- react-query
- 머신러닝
- CSS
- 프론트엔드
- 리액트
- testing
- HTML
- TypeScript
- next.js
- 딥러닝
- jest
- styled-components
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |