-
[Next.js] 5. Dynamic Routes공부 2021. 10. 27. 17:03
블로그 데이터로 index 페이지를 채우긴 했지만, 아직 개별 블로그 페이지를 만들지는 않았다. 이러한 페이지의 URL이 블로그 데이터에 따르기를 원하므로 동적 라우트를 사용해야 한다.
Page Path Depends on External Data
이전 강의에서 우리는 페이지 내용이 외부 데이터에 의존하는 경우를 다뤘다. index 페이지를 렌더링하기 위해서 필요한 데이터를 가져오는 getStaticProps를 사용했다.
이번 강의에서는 각 페이지 경로가 외부 데이터에 의존하는 경우를 다룬다. Next.js는 외부 데이터에 의존하는 경로들을 정적으로 생산할 수 있게 하며, 이는 Next.js에서 동적 URL을 가능하게 한다.
How to Statically Generate Pages with Dynamic Routes
우리의 경우, 블로그 게시물들을 위한 동적 라우팅을 만든다.
- 각 포스트가 /posts/<id> 의 경로를 가진다. <id>는 최상위 디렉토리인 posts 아래에 있는 마크다운 파일의 이름이다.
- ssg-ssr.md와 pre-rendering.md 파일이 있으므로, 우리는 /posts/ssg-ssr과 /posts/pre-redering으로 경로를 정한다.
Overview of the Steps
이를 위해서 다음 단계를 따른다. -다음페이지에서 할 예정이니 아직 변경하지 않아도 된다.
1. [id].js 라는 파일을 pages/posts 아래에 만든다. [ 로 시작하고 ] 로 닫히는 페이지들은 Next.js에서 동적 경로이다.
pages/posts/[id].js에서 post페이지(우리가 만들었던 다른 페이지들과 마찬가지)를 렌더링하는 코드를 작성한다.
import Layout from '../../components/layout' export default function Post() { return <Layout>...</Layout> }
2. 새로운 사항들: 이 페이지에서 getStaticPaths 라는 비동기 함수를 내보낼것이다. 이 함수에서 id에 대해 가능한 값의 목록을 반환해야 한다.
import Layout from '../../components/layout' export default function Post() { return <Layout>...</Layout> } export async function getStaticPaths() { // Return a list of possible value for id }
3. getStaticProps를 다시 실행한다. 이번에는 주어진 id로 블로그 게시물에 필요한 정보를 가져온다. getStaticProps는 id를 포함한 params가 주어진다. (파일 이름이 [id].js 이기 때문에)
import Layout from '../../components/layout' export default function Post() { return <Layout>...</Layout> } export async function getStaticPaths() { // Return a list of possible value for id } export async function getStaticProps({ params }) { // Fetch necessary data for the blog post using params.id }
이제까지 우리가 위에서 이야기 했던 이야기 요약본이다.
Implement getStaticPaths
1. 파일을 세팅하자.
- [id].js 라는 파일을 pages/posts 디렉토리안에 만든다.
- pages/posts 디렉토리안에 있는 first-post.js 파일을 삭제한다. (더이상 사용하지 않을것이기 때문)
2. pages/posts/[id].js 파일을 열고 아래의 코드를 작성한다. ... 부분은 나중에 채워넣을 것이다.
import Layout from '../../components/layout' export default function Post() { return <Layout>...</Layout> }
3. lib/posts.js 파일을 열고 아래에 있는 getAllPostIds 함수를 추가한다. 이는 posts 디렉토리에 있는 파일 이름들(.md는 제외한)의 리스트를 반환할 것이다.
export function getAllPostIds() { const fileNames = fs.readdirSync(postsDirectory) // Returns an array that looks like this: // [ // { // params: { // id: 'ssg-ssr' // } // }, // { // params: { // id: 'pre-rendering' // } // } // ] return fileNames.map(fileName => { return { params: { id: fileName.replace(/\.md$/, '') } } }) }
* 중요: 반환된 목록은 단순한 문자 배열이 아니라 위의 주석과 같은 객체 배열이어야 한다. 각 객체는 params 키를 가지고 있어야 하며, id 키를 가진 오브젝트를 포함해야 한다. (파일 이름에 있는 [id]를 사용할 것이기 때문) 이렇게 하지 않으면 getStaticPaths는 실패한다.
4. getAllPostIds 함수를 import하고 getStaticPaths안에서 사용한다. pages/posts/[id].js를 열고 아래 코드를 내보낸 Post컴포넌트 위에 작성한다.
import { getAllPostIds } from '../../lib/posts' export async function getStaticPaths() { const paths = getAllPostIds() return { paths, fallback: false } }
- paths는 getAllPostIds()에 의해 반환된 알려진 경로의 배열을 포함하며, pages/posts/[id].js에 의해 정의된 매개변수를 포함한다.
- fallback: false 는 지금은 무시한다. -나중에 설명할 것이다.
Implement getStaticProps
주어진 id를 가진 게시물을 렌더링하기 위해서 필요한 데이터를 가져와야 한다.
1. 그러기 위해서는 lib/posts.js 파일을 열고 아래의 getPostData 함수를 하단에 추가한다. 이는 id에 기반한 게시물 데이터를 반환할 것이다.
export function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Combine the data with the id return { id, ...matterResult.data } }
2. pages/posts/[id].js 파일을 열고 아래의 코드를
import { getAllPostIds } from '../../lib/posts'
아래의 코드로 바꾼다.
import { getAllPostIds, getPostData } from '../../lib/posts' export async function getStaticProps({ params }) { const postData = getPostData(params.id) return { props: { postData } } }
현재 게시물 페이지는 게시글 데이터를 걷고 props로 반환하기 위해 getStaticProps 안에서 getPostData 함수를 사용하고 있다.
3. 이제 postData를 사용해서 Post 컴포넌트를 업데이트 한다. pages/posts/[id].js 파일에서 내보낸 Post 컴포넌트를 다음 코드로 바꾼다.
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} </Layout> ) }
아래의 페이지를 방문하면 각 페이지의 블로그 데이터를 볼 수 있다.
동적 라우팅을 성공적으로 생성했다.
Something Wrong?
만약 오류가 발생한다면 파일에 올바른 코드가 있는지 확인한다.
- pages/posts/[id].js 는 이런식으로 작성해야 한다.
- lib/posts.js는 이런식으로 작성해야 한다.
- 만약 아직도 작동하지 않는다면, 나머지 코드들은 이런식으로 작성해야 한다.
Summary
Render Markdown
1. 마크다운 콘텐츠를 렌더링하기 위해 remark 라이브러리를 사용할 것이다. 먼저 설치해보자.
yarn add remark@13 remark-html@13
2. lib/posts.js 파일을 열고 아래 코드를 파일 최상단에 import 한다.
import remark from 'remark' import html from 'remark-html'
3. 같은 파일에서 getPostData() 함수를 remark를 이용해서 다음과 같이 업데이트 한다.
export async function getPostData(id) { const fullPath = path.join(postsDirectory, `${id}.md`) const fileContents = fs.readFileSync(fullPath, 'utf8') // Use gray-matter to parse the post metadata section const matterResult = matter(fileContents) // Use remark to convert markdown into HTML string const processedContent = await remark() .use(html) .process(matterResult.content) const contentHtml = processedContent.toString() // Combine the data with the id and contentHtml return { id, contentHtml, ...matterResult.data } }
* 중요: remark를 위해 await를 사용해야하기 때문에 getPostData에 async 키워드를 추가했다. async/await는 데이터를 비동기적으로 가져올 수 있다.
4. 즉, getPostData를 호출 할 때 await를 사용하려면 pages/posts/[id].js 파일의 getStaticProps를 업데이트 해야한다.
export async function getStaticProps({ params }) { // Add the "await" keyword like this: const postData = await getPostData(params.id) // ... }
5. dangerouslySetInnerHTML를 사용하는 contentHtml을 렌더링 하기 위해서 pages/posts/[id].js 파일에 있는 Post 컴포넌트를 업데이트 한다.
export default function Post({ postData }) { return ( <Layout> {postData.title} <br /> {postData.id} <br /> {postData.date} <br /> <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> </Layout> ) }
아래의 페이지를 방문하면 각 페이지의 블로그 데이터를 볼 수 있다.
Polishing the Post Page
Adding title to the Post Page
pages/posts/[id].js 파일에서 게시글 데이터를 이용해 title 태그를 추가한다. 파일 상단에 next/head를 import 하고, Post 컴포넌트를 업데이트 함으로써 title 태그를 추가한다.
// Add this import import Head from 'next/head' export default function Post({ postData }) { return ( <Layout> {/* Add this <Head> tag */} <Head> <title>{postData.title}</title> </Head> {/* Keep the existing code here */} </Layout> ) }
Formatting the Date
1. 날짜 형식을 지정하기 위해서 date-fns 라이브러리를 사용한다. 먼저 설치한다.
yarn add date-fns
2. components/date.js 라는 파일을 만들고 다음의 Date 컴포넌트를 추가한다.
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> }
* date-fns 웹 사이트에서 다양한 format() 문자열 옵션을 볼 수 있다.
3. pages/posts/[id].js 파일을 열어 상단에 Date 컴포넌트를 import 하고, {postData.date}대신 사용한다.
// Add this import import Date from '../../components/date' export default function Post({ postData }) { return ( <Layout> {/* Keep the existing code here */} {/* Replace {postData.date} with this */} <Date dateString={postData.date} /> {/* Keep the existing code here */} </Layout> ) }
http://localhost:3000/posts/pre-rendering에 방문해 본다면, 날짜가 2021-10-26에서 October 26, 2021로 변경된 것을 볼 수 있다.
Adding CSS
이전에 우리가 만들었던 styled/utils.module.css 파일을 사용해서 몇가지 CSS를 추가한다. pages/posts/[id].js 파일을 열어 CSS 파일을 import 하고, Post 컴포넌트를 아래의 코드로 변경한다.
// Add this import at the top of the file import utilStyles from '../../styles/utils.module.css' export default function Post({ postData }) { return ( <Layout> <Head> <title>{postData.title}</title> </Head> <article> <h1 className={utilStyles.headingXl}>{postData.title}</h1> <div className={utilStyles.lightText}> <Date dateString={postData.date} /> </div> <div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} /> </article> </Layout> ) }
http://localhost:3000/posts/pre-rendering에 방문해 본다면, 아래와 같이 변경된 것을 볼 수 있다.
Polishing the Index Page
index 페이지(pages/index.js)를 업데이트 하자. Link 컴포넌트를 사용해 각 게시물 페이지에 링크를 준다.
1. pages/index.js 파일을 열고 파일 상단에 아래와 같은 Link와 Date 를 import 한다.
import Link from 'next/link' import Date from '../components/date'
2. 같은 파일의 Home 컴포넌트 하단 근처에 있는 li 태그를 아래 코드로 변경한다.
<li className={utilStyles.listItem} key={id}> <Link href={`/posts/${id}`}> <a>{title}</a> </Link> <br /> <small className={utilStyles.lightText}> <Date dateString={date} /> </small> </li>
http://localhost:3000에 방문해 본다면, 아래와 같이 각 게시물에 대한 링크가 있는 것을 볼 수 있다.
Dynamic Routes Details
동적 라우팅에 대해 필수적으로 알아야 할 몇가지 정보들이다.
Fetch External API or Query Database
getStaticProps와 똑같이 getStaticPaths는 소스가 무엇이든지 데이터를 가져올 수 있다. 우리의 예제에서는 getAllPostIds가 외부 API로부터 가져올 수 있다.
export async function getAllPostIds() { // Instead of the file system, // fetch post data from an external API endpoint const res = await fetch('..') const posts = await res.json() return posts.map(post => { return { params: { id: post.id } } }) }
Development v.s. Production
- 개발(npm run dev 또는 yarn dev)에서 getStaticPaths는 모든 요청에 구동된다. (getStaticProps와 동일)
- 운영에서 getStaticPaths는 빌드 시에 구동된다.
Fallback
getStaticPaths로부터 fallback: false 를 반환했던 것을 상기해보자. 무엇을 의미하는가?
만약 fallback이 false라면, getStaticPaths에 의해 반환되지 않은 모든 경로는 404 페이지가 된다.
만약 fallback이 true라면, getStaticProps 행동은 변한다:
- getStaticPaths에 의해 반환된 경로들은 빌드 시에 HTML로 렌더될 것이다.
- 빌드 시에 생성되지 않은 경로들은 404 페이지가 된다. 대신 Next.js는 그러한 경로에 대한 첫 요청에서 해당 페이지의 fallback(대체) 버전을 제공한다.
- 백그라운드에서 Next.js는 요청된 경로를 덩적으로 생성한다. 동일한 경로에 대한 후속 요청은 빌드 시 미리 렌더링 된 다른 페이지와 마찬가지로 생성된 페이지를 제공한다.
만약 fallback이 blocking이라면 새로운 경로는 getStaticPorps로 서버사이드 렌더링 되고, 향후 요청을 위해 저장되므로 경로당 한번만 발생한다.
Catch-all Routes
동적 라우팅은 대괄호 안에 점 세개( ... )를 추가함으로써 모든 경로를 잡을 수 있도록 확장할 수 있다. 예:
- pages/posts/[...id].js 는 /posts/a를 잡을 수 있지만, /posts/a/b, /post/a/b/c 등등 또한 잡을 수 있다.
이렇게 하면 getStaticPaths에서 다음과 같이 id 키 값으로 배열을 반환해야 한다.
return [ { params: { // Statically Generates /posts/a/b/c id: ['a', 'b', 'c'] } } //... ]
그리고 params.id는 getStaticProps의 배열이 된다:
export async function getStaticProps({ params }) { // params.id will be like ['a', 'b', 'c'] }
Router
Next.js 라우터에 접근하려면 next/router에서 userRouter 훅을 import해서 액세스하면 된다.
404 Pages
404페이지를 커스텀하기 위해서 pages/404.js 파일을 만든다. 이 파일은 빌드 시 정적으로 생성된다.
// pages/404.js export default function Custom404() { return <h1>404 - Page Not Found</h1> }
'공부' 카테고리의 다른 글
[Next.js] 7. Deploying Your Next.js App (0) 2021.10.29 [Next.js] 6. API Routes (0) 2021.10.28 [Next.js] 4. Pre-rendering and Data Fetching (0) 2021.10.26 [Next.js] 3. Assets, Metadata, and CSS (0) 2021.10.25 [Next.js] 2. Navigate Between Pages (0) 2021.10.22