블로그 데이터로 인덱스 페이지를 채웠습니다. 그렇지만, 개인 블로그 페이지를 만들지는 않았습니다. (우리가 원하는 결과).
블로그 데이터에 따라 이 페이지들에 URL 을 주고 싶습니다. 동적 라우트가 필요하다는 뜻입니다.
이번 레슨에서 배울것들:
이전 레슨부터 계속하고 있다면, 이 페이지를 건너뛰어도 됩니다. 3. 으로 이동하세요.
스타터 코드 다운로드(선택적)
이전 레슨부터 계속하고 있지 않다면, 이 레슨 아래의 스타터 코드를 다운로드, 설치 및 실행할 수 있습니다. 이전 레슨의 결과와 동일하게 'Next-js-blog' 디렉토리를 설정합니다.
다시, 만약 당신이 이전 레슨을 완료했다면 이 작업은 필요하지 않습니다.
npm init next-app nextjs-blog --example "https://github.com/zeit/next-learn-starter/tree/master/dynamic-routes-starter"
그리고 커맨드 출력의로부터의 명령을 따라하세요. ('cd'로 디렉토리로 이동한 다음, 개발 서버를 시작하세요)
또한 다음의 파일들을 업데이트 해야합니다.
'public/images/profile.jpg' 의 이름을 가진 당신의 사진 (추천 해상도: 400px 폭/높이)
'components/layout.js' 안의 변수 'const name = '[Your Name]' 에 당신의 이름.
'pages/index.js' 안의 태그 '
[Your Self Introduction]
에 당신의 자기소개.이전 레슨에서, 우리는 외부 데이터에 따른 페이지 내용을 다루었습니다. 요청된 데이터를 가져와 인덱스 페이지를 렌더링하기 위해 'getStaticProps' 를 사용했었죠.
이 레슨에선, 각 페이지 경로가 외부 데이터에 의존하는 경우에 대해서 이야기 할 것입니다. Next.js 는 외부 데이터로 경로 및 페이지를 정적으로 생성하게 해줍니다. 그러면 Next.js 에서 동적 URL 을 사용할 수 있습니다.
우리의 경우에서는, 블로그 포스트를 위해 동적으로 페이지를 생성하고 싶습니다:
아래의 단계를 거치면 우리도 할 수 있습니다. 아직 이 작업을 따라 할 필요는 없습니다 — 다음 페이지에서 다 할 거에요.
첫 째, 'pages/post' 디렉토리 밑에 '[id.js]' 라는 페이지를 생성할 것입니다. Next.js '[' 로 시작해서 ']'로 끝나는 페이지들은 동적 페이지들 입니다.
'pages/posts/[id].js' 안에, 포스트 페이지에서 렌더링 될 코드를 작성할 것입니다. — 우리가 만들었던 다른 페이지들처럼요.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
새로운 것: 우리는 이 페이지로부터 'getStaticPaths' 라는 async 함수를 내보낼 것입니다. 이 함수 안에서, '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
}
마지막으로, 'getStaticProps' 를 다시 구현해야 합니다. 이번에는 주어진 'id'로 블로그 포스트에 필요한 데이터들을 가져 옵니다. 'getStaticProps' 에는 'id' 를 포함하는 'params' 가 제공 됩니다.
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
}
무슨 얘기 한건지 그림으로 요약:
다음 페이지에서 동적 라우팅을 시도해보죠!
먼저, 파일들을 세팅합시다:
그리고, 'pages'posts'[id].js' 에 아래 내용을 채워 넣으세요. '...' 의 부분은 나중에 채울 것입니다.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
그리고 'lib/posts/js' 를 열고 이 함수를 추가하세요. 이 함수는 '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' 는 실패할 것입니다.
마지막으로 'pages/posts/[id].js' 내에 이 함수를 불러올 것입니다.
import { getAllPostIds } from '../../lib/posts'
그리고 이 함수를 호출하는 'getStaticPaths' 를 생성하세요.
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
거의 다 했습니다 — 그러나 여전히 'getStaticProps' 를 구현해야 합니다. 다음 페이지에서 해볼까요?
주어진 'id'로 포스트를 렌더링하기 위해 필요한 데이터를 가져와야 합니다.
그렇게 하려면 'lib.posts.js'를 다시 열고 이 함수를 추가하세요. 이 함수는 '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
}
}
마지막으로, 'pages/posts/[id].js' 안의 이 라인을:
import { getAllPostIds } from '../../lib/posts'
이렇게 고치세요:
import { getAllPostIds, getPostData } from '../../lib/posts'
그리고, 이 함수를 부르는 'getStaticProps' 를 생성하세요:
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
그 다음 'Post' 컴포넌트를 'postData' 를 사용해 업데이트 하세요:
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
이게 전부입니다! 이 페이지들을 방문해보세요:
잘했습니다! 성공적으로 동적 페이지를 생성했습니다.
에러와 싸우고 있다면, 파일들이 맞는 코드를 가지고 있는지 확인해보세요:
여전히 삽질 중이라면, GitHub Discussions 커뮤니티에 자유롭게 질문 하세요. 당신의 코드를 GitHub 에 푸쉬하고 다른 사람들이 볼 수 있게 링크할 수 있다면 도움이 될 것입니다.
우리가 한 일을 또 그림으로 요약했어요:
그러나 우리는 여전히 블로그 마크다운 컨텐츠를 화면에 표시하지 않았습니다. 다음에 해보죠.
마크다운 컨텐츠를 렌더링하려고 'remark' 라이브러리를 사용할 것입니다. 먼저, 설치 해봅시다:
npm install remark remark-html
'lib/posts.js'에 가져오세요:
import remark from 'remark'
import html from 'remark-html'
그리고 'remark' 를 사용해 아래처럼 'getPostData()' 를 수정하세요.
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
}
}
중요: 'getPostData' 에 'async' 키워드를 추가했습니다. 'remark' 에 'await' 를 사용하는데 'async' 키워드가 필요하기 때문입니다.
'getPostData' 를 호출 할 때 'pages/posts/[id].js' 에서 'getStaticProps' 를 업데이트 하는데 'await' 를 써야 합니다:
export async function getStaticProps({ params }) {
// 'await' 키워드를 아래와 같이 추가하세요:
const postData = await getPostData(params.id)
// ...
}
마지막으로 'dangerouslySetInnerHTML' 을 사용해 'contentHtml'를 렌더링하도록 'Post' 컴포넌트를 업데이트 합니다:
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
)
}
다시 한번 이 페이지들을 방문해보세요:
이런 블로그 컨텐츠가 보여야 합니다:
거의 다했어요! 페이지를 좀 더 다듬어보죠
'pages/posts/[id].js' 내에 포스트 데이터를 사용해 'title' 태그를 추가해봅시다. 'next/head/를 가져오고 'title' 태그를 추가하세요:
import Head from 'next/head'
export default function Post({ postData }) {
return (
<Layout>
<Head>
<title>{postData.title}</title>
</Head>
...
</Layout>
)
}
날짜 서식 지정하기
날짜의 형식을 지정하기 위해, 'date-fns' 라이브러리를 사용할 것입니다. 먼저, 설치하세요:
npm intall date-fns
다음, '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>
}
그리고, 'pages/posts/[id].js' 에 'Date' 컴포넌트를 사용하세요.
// Add this line to imports
import Date from '../../components/date'
export default function Post({ postData }) {
return (
<Layout>
...
{/* {postData.date} 를 바로 아랫줄 코드처럼 고치세요. */}
<Date dateString={postData.date} />
...
</Layout>
)
}
http://localhost:3000/posts/pre-rendering 에 접속하면, 날짜가 "January 1, 2020" 이라고 적힌것이 보여야 합니다.
마지막으로, 'pages/posts/[id].js' 내에 CSS를 추가해봅시다. 모든 것을 'article' 태그 밑에 넣고 CSS 모듈을 아래와 같이 사용합니다.
// Add this line
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 에 접속하면 페이지가 조금 더 나아 보일 겁니다:
잘했어요! 인덱스 페이지를 가다듬고 끝내죠!
마지막 단계로, 인덱스 페이지를 업데이트 해봅시다. ('pages/index.js').
특히, 각 포스트 페이지에 링크들을 추가해야 합니다. 'Link' 컴포넌트를 사용할 것이지만, 이번엔 약간 다르게 할 필요가 있습니다.
동적 라우트 페이지에 링크를 걸려면, 'Link' 컴포넌트를 다르게 사용해야 합니다. 우리의 경우 'posts/ssg-ssr' 로 링크를 걸기 위해, 다음과 같이 코드를 작성할 필요가 있습니다.
<Link href="/posts/[id]" as="/posts/ssg-ssr">
<a>...</a>
</Link>
볼 수 있듯, '[id]'를 'href' 에 사용하고 실제 경로('ssg-ssr')를 'as' 의 prop 으로 사용해야 합니다.
이걸 구현해보죠, 먼저, 'Link' 를 'pages/index.js' 의 'Date' 에 가져옵니다:
import Link from 'next/link'
import Date from '../components/date'
그리고, 하단의 'Home' 컴포넌트 근처의 'li' 태그를 아래와 같이 수정합니다:
<li className={utilStyles.listItem} key={id}>
<Link href="/posts/[id]" as={`/posts/${id}`}>
<a>{title}</a>
</Link>
<br />
<small className={utilStyles.lightText}>
<Date dateString={date} />
</small>
</li>
이제, 각 아티클들은 링크를 가져야 합니다.
만약 뭔가 안된다면, 당신의 코드가 이렇게 보이는지 확인해보세요.
이게 전부입니다! 이 레슨을 마무리하기 전, 다음 페이지에서 동적 라우팅에 대한 몇가지 팁에 대해 얘기 해봅시다.
우리 도큐먼테이션에서 동적 라우트에 대한 자세한 정보를 얻을 수 있습니다:
그러나 당신이 알아야할 동적 라우팅에 대한 본질적인 정보들이 있습니다.
'getStaticProps'처럼 'getStaticPath'는 어떤 소스로부터든 데이터를 가져올 수 있습니다. 우리 예제에서, 'getAllPostIds' ('getStaticPaths' 에 의해 사용됨) 는 외부 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
}
}
})
}
'getStaticPaths'로 부터 'fallback: false'를 리턴했던것을 떠올려보세요. 이게 무슨 뜻일까요?
'fallback'이 false 면, 'getStaticPaths' 에서 리턴되지 않은 경로는 404 페이지가 됩니다.
'fallback' 이 true 면, 'getStaticProps' 의 행동이 변합니다:
'getStaticPaths' 로 부터 리턴 된 경로는 빌드 시 HTML로 렌더링 될 것입니다.
빌드 시 생성되지 않은 페이지들은 404 페이지가 되지 않습니다. 대신, Next.js 는 이런 경로에 대한 첫 번째 요청에서 페이지의 "fallback" 버전을 제공할 것입니다.
백그라운드에서, Next.js 가 요청된 경로를 정적으로 생성합니다. 같은 경로에 들어온 후속요청은 그 생성된 페이지가 제공됩니다. 빌드 시 사전-렌더링 된 다른 페이지들처럼 말이죠.
이것은 우리 레슨의 범위의 너머입니다.하지만, 도큐먼테이션에서 'fallback: true' 에 대해 더 학습해 볼 수 있습니다.
동적 라우트는 괄호 안에 세 개의 점('...')을 추가하는 것으로 모든 경로를 잡게 확장될 수 있습니다. 예를 들면:
'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']
}
동적 라우트에 대한 도큐먼테이션을 보고 더 학습하세요.
'useRouter' hook 를 'next/router' 에서 가져오는 것으로 Next.js 라우터에 접근할 수 있습니다. 라우터 도큐먼테이션을 보고 더 학습하세요.
커스텀 404 페이지를 만들려면, 'pages/404.js' 를 생성하세요. 이 파일은 빌드 시 정적으로 생성됩니다.
// pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
에러 페이지에 대한 도큐먼테이션을 보고 더 학습 하세요.
'getStaticProps' 와 'getStaticPaths' 를 설명하기 위해 몇가지 예제를 만들었습니다. — 소스코드를 보고 더 학습하세요.
다음 레슨에서, Next.js 의 API 라우터 기능에 대해 얘기 해 봅시다.