※ Next.JS의 공식문서 튜토리얼을 번역합니다
※ 각 챕터 내 Setup 파트는 생략했습니다. 실습하던 프로젝트로 계속 진행하시면 됩니다.
※ 필자 입맛대로 한 번역이므로 주의 요함
※ 오역, 오타에 대한 피드백 두 팔 벌려 환영
우리는 블로그를 만들기 원하지만, 아직까진 블로그 컨텐츠를 등록하지 않았어요. 이번엔 외부의 블로그 데이터를 어떻게 우리의 앱으로 가져오는지에 대해 배워볼 겁니다. 이 튜토리얼에선 블로그 컨텐츠를 파일 시스템 내부에 저장할 거지만, 컨텐츠는 어디에 저장하든 상관 없이 불러와 집니다. (예를 들면, DB나 Headless CMS 같은 곳)
이 레슨에서 당신이 배우게 될 것은,
getStaticProps
를 사용하여 외부 블로그 데이터를 페이지로 import 해 오는 방법getStaticProps
에 관한 유용한 정보데이터 가져오는 법을 배우기 앞서, Next.js의 중요한 개념인 프리렌더링에 대해 먼저 얘기해 볼게요.
기본적으로 Next.js는 모든 페이지를 프리렌더링합니다. 이 말인 즉슨, Next.js는 클라이언트 사이드의 자바스크립트로 페이지에 대한 전체 HTML을 생성하는 대신에 각 페이지에 대한 HTML을 사전에 생성합니다. 프리렌더링은 퍼포먼스와 SEO의 측면에 있어 더 나은 결과를 가집니다.
생성된 HTML은 해당 페이지에 필요한 최소한의 자바스크립트 코드로 되어있습니다. 페이지가 브라우저에 의해 로드될 때, 페이지의 자바스크립트 코드가 실행되고 페이지를 완벽히 인터렉티브하게 만듭니다. (이 과정을 hydration(수화)라고 합니다.)
아래의 과정을 거쳐야 프리렌더링이 발생하는 것을 확인할 수 있습니다.
자바스크립트 없이도 당신의 앱이 렌더링 되는 것을 볼 수 있습니다. 왜냐하면 Next.js는 자바스크립트 없이도 앱의 UI를 볼 수 있도록 하면서 앱을 정적 HTML로 프리렌더링하니깐요.
Pre-rendering (Next.js 사용)
<Link />
와 같은 인터렉티브한 컴포넌트를 가지고 있다면, 해당 요소는 JS가 로드된 후에 활성화됩니다. No Pre-rendering (기본 리액트 앱)
다음은 Next.js의 두 가지 프리렌더링 형태를 알아 보겠습니다.
Next.js는 정적 생성과 서버 사이드 렌더링이라는 두 가지 형태의 프리렌더링을 제공합니다. 이 둘의 차이점은 언제 HTML이 생성되느냐예요.
개발모드에선(
npm run dev
혹은yarn dev
명령어로 실행시켰을 때), 모든 페이지는 요청시에 프리렌더링 됩니다. 정적 생성을 사용하는 페이지에서도 말이죠.
중요한 점은, Next.js는 각 페이지에 대한 렌더링 방식을 선택할 수 있도록 한다는 겁니다. 여러분은 대부분의 페이지를 정적 생성 방식으로 렌더링하고 나머지 페이지는 서버 사이드 렌더링 방식으로 렌더링하는 '하이브리드'한 Next.js 앱을 만들 수 있어요.
우리는 가능하면 정적 생성 방식을 사용하는 걸 추천합니다. 왜냐하면 당신의 페이지는 CDN이라는 요청 때마다 서버 렌더링 되는 페이지보다 더 빠르게
당신은 다양한 종류의 페이지에 정적 생성 방식을 사용할 수 있습니다. 가령,
자신에게 물어 보세요. "이 페이지는 유저가 요청하기 전에 미리 프리렌더링 되어야 할까요?" 만약 당신의 대답이 "네"라면, 정적 생성 방식을 선택해야 합니다.
반면에, 유저의 요청보다 먼저 프리렌더링 할 수 없는 페이지의 경우 정적 생성은 좋은 방안이 아닙니다. 당신의 페이지가 꾸준히 업데이트되는 데이터를 보여준다면, 페이지 컨텐츠는 요청 때마다 바뀌어야 합니다.
이런 경우, 서버 사이드 렌더링을 사용합니다. 속도가 보다 느릴 수는 있지만, 프리렌더링 된 페이지는 항상 최신의 상태이죠. 아니면 프리렌더링을 건너뛰고, 자바스크립트를 이용한 클라이언트 사이드 렌더링을 사용해도 됩니다.
이 레슨에서, 우리는 정적 생성에 중점을 둘 겁니다. 다음 페이지에선 데이터 유무에 따른 정적 생성 방식에 대해 얘기해 볼게요.
정적 생성은 데이터가 있든 없든 잘 작동합니다.
지금까지 우리가 만든 모든 페이지는 외부 데이터를 가져올 필요가 없었죠. 이런 페이지들은 앱이 배포를 위해 빌드될 때, 자동적으로 정적 생성 방식을 따릅니다.
하지만 어떤 페이지는 외부 데이터 없이는 HTML 렌더링이 불가능할 수도 있습니다. 빌드 타임에 파일 시스템에 접근해야 될 수도, 외부 API 를 가져와야 할 수도, 데이터 베이스 쿼리를 실행해야 할 수도 있겠죠. Next.js는 이러한 경우도 데이터가 있는 정적 생성 방식으로 지원합니다.
어떻게 그게 가능할까요? Next.js에서 페이지 컴포넌트를 export할 때, 당신은 getStaticProps
라는 async
함수도 export 할 수 있습니다.
getStaticProps
는 배포 단계에서 빌드 타임 때 작동합니다. 그리고..export default function Home(props) { ... }
export async function getStaticProps() {
// Get external data from the file system, API, DB, etc.
const data = ...
// The value of the `props` key will be
// passed to the `Home` component
return {
props: ...
}
}
근본적으로, getStaticProps
를 사용한다는 것은 Next.js로 하여금 "야, 이 페이지는 데이터를 활용하니깐 빌드 타임에 이 페이지 프리렌더링 할 때, 데이터부터 먼저 해결해!" 라고 말하는 격입니다.
Note : 개발 모드에서,
getStaticProps
는 요청시에 작동합니다.
실습이 더 배우기 쉽죠. 다음 페이지에선 getStaticProps
를 사용해서 우리의 블로그를 만들어 봅시다.
이제 파일 시스템을 사용해서 앱에 블로그 데이터를 추가해 볼 겁니다. 각각의 블로그 포스트는 마크다운 파일로 되어 있어요.
posts
라는 상위 폴더를 생성하세요.(pages/posts
와는 다른 겁니다.)posts
안에, pre-rendering.md
와 ssg-ssr.md
라는 두 개의 파일을 만드세요.posts/pre-rendering.md
안에 아래의 코드를 넣어 주세요.
---
title: 'Two Forms of Pre-rendering'
date: '2020-01-01'
---
Next.js has two forms of pre-rendering: **Static Generation** and **Server-side Rendering**. The difference is in **when** it generates the HTML for a page.
- **Static Generation** is the pre-rendering method that generates the HTML at **build time**. The pre-rendered HTML is then _reused_ on each request.
- **Server-side Rendering** is the pre-rendering method that generates the HTML on **each request**.
Importantly, Next.js lets you **choose** which pre-rendering form to use for each page. You can create a "hybrid" Next.js app by using Static Generation for most pages and using Server-side Rendering for others.
posts/ssg-ssr.md
에 아래의 코드를 넣어 주세요.
---
title: 'When to Use Static Generation v.s. Server-side Rendering'
date: '2020-01-02'
---
We recommend using **Static Generation** (with and without data) whenever possible because your page can be built once and served by CDN, which makes it much faster than having a server render the page on every request.
You can use Static Generation for many types of pages, including:
- Marketing pages
- Blog posts
- E-commerce product listings
- Help and documentation
You should ask yourself: "Can I pre-render this page **ahead** of a user's request?" If the answer is yes, then you should choose Static Generation.
On the other hand, Static Generation is **not** a good idea if you cannot pre-render a page ahead of a user's request. Maybe your page shows frequently updated data, and the page content changes on every request.
In that case, you can use **Server-Side Rendering**. It will be slower, but the pre-rendered page will always be up-to-date. Or you can skip pre-rendering and use client-side JavaScript to populate data.
각각의 마크다운 파일의 윗부분에
title
과date
를 포함한 메타데이터 섹션이 있다는 걸 눈치챘을 거예요. 이는 YAML Front Matter라고 불리는데, gray-matter라는 라이브러리를 이용해서 파싱할 수 있습니다.
지금부터 데이터를 활용하여 우리의 메인 페이지(pages/index.js
)를 업데이트 해 봅시다.
title
, date
, 파일명(post URL을 위한 id 값으로 쓰일 겁니다.)을 가져옵니다.프리렌더링이 이루어지게 하기 위해 getStaticProps
를 사용합니다.
다음 페이지에서 해 봅시다!
먼저, 마크다운 파일 안에 있는 메타데이터를 파싱하기 위해 gray-matter을 설치합니다.
npm install gray-matter
그 다음, 파일 시스템에서 데이터를 가져오는 간단한 라이브러리를 생성해 줍니다.
lib
이라는 상위 폴더를 생성합니다.lib
폴더 안에 posts.js
라는 파일을 생성하고 아래의 코드를 넣어 주세요.import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'
const postsDirectory = path.join(process.cwd(), 'posts')
export function getSortedPostsData() {
// /posts 폴더 아래의 파일들 가져오기
const fileNames = fs.readdirSync(postsDirectory)
const allPostsData = fileNames.map(fileName => {
// 파일명을 id 값으로 활용하기 위해 파일명에서 '.md' 확장자 제거
const id = fileName.replace(/\.md$/, '')
// 마크다운 파일을 문자열로 읽어 들이기
const fullPath = path.join(postsDirectory, fileName)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// gray-matter 이용하여 포스트의 메타데이터 파싱하기
const matterResult = matter(fileContents)
// 데이터와 id 묶기
return {
id,
...matterResult.data
}
})
// 날짜를 기준으로 정렬하기
return allPostsData.sort(({ date: a }, { date: b }) => {
if (a < b) {
return 1
} else if (a > b) {
return -1
} else {
return 0
}
})
}
이제 getSortedPostsData
를 import 해 온 후, pages/index.js
의 getStaticProps
안에서 호출해야 합니다.
pages/index.js
파일을 열고, 아래의 코드를 Home
컴포넌트 위에 넣어 주세요.
import { getSortedPostsData } from '../lib/posts'
export async function getStaticProps() {
const allPostsData = getSortedPostsData()
return {
props: {
allPostsData
}
}
}
getStaticProps
안에 있는 props
객체의 allPostsData
를 리턴함으로써, 블로그 포스트 데이터는 Home
컴포넌트에 props로 전달될 겁니다. 이제 블로그 포스트 데이터에 아래와 같이 접근할 수 있어요.
export default function Home ({ allPostsData }) { ... }
블로그 포스트를 보여주기 위해, Home
컴포넌트의 자기소개 섹션 아래에 또다른 <section>
태그를 추가해 주세요. props를 ()
에서 ({ allPostsData })
로 변경하는 것도 잊지 마시구요.
export default function Home({ allPostsData }) {
return (
<Layout home>
{/* 기존 코드 자리 */}
{/* 새로운 <section> 태그를 추가해 주세요*/}
<section className={`${utilStyles.headingMd} ${utilStyles.padding1px}`}>
<h2 className={utilStyles.headingLg}>Blog</h2>
<ul className={utilStyles.list}>
{allPostsData.map(({ id, date, title }) => (
<li className={utilStyles.listItem} key={id}>
{title}
<br />
{id}
<br />
{date}
</li>
))}
</ul>
</section>
</Layout>
)
}
이제 이 URL에 접속하면, 블로그 데이터가 보일 거예요.
축하합니다! 우린 성공정으로 파일 시스템으로부터 외부 데이터를 가져와 메인 페이지를 프리렌더링했어요.
다음 페이지에선 getStaticProps
에 관한 유용한 팁에 대해 얘기해 보겠습니다.
getStaticProps
에 대해 꼭 알고 있어야 하는 정보를 알려 드릴게요.
우리는 getSortedPostsData
라는 함수로 파일시스템에서 데이터를 가져오도록 하여lib/posts/js
에서 실행시켰어요. 하지만 여러분은 외부 API의 엔드포인트와 같이 다른 자원으로부터 데이터를 가져올 수 있고, 이 또한 잘 작동할 겁니다.
export async function getSortedPostsData() {
// Instead of the file system,
// fetch post data from an external API endpoint
const res = await fetch('..')
return res.json()
}
Note : Next.js 클라이언트단과 서버단에서 모두에서
fetch()
함수를 폴리필합니다. 그러므로 import 할 필요 없습니다.
데이터베이스에 대한 쿼리를 직접 작성할 수도 있어요.
import someDatabaseSDK from 'someDatabaseSDK'
const databaseClient = someDatabaseSDK.createClient(...)
export async function getSortedPostsData() {
// Instead of the file system,
// fetch post data from a database
return databaseClient.query('SELECT posts...')
}
이러한 점이 가능한 건 getStaticProps
가 오직 서버단에서만 작동하기 때문입니다. 절대 클라이언트단에선 작동하지 않습니다. 브라우저를 위한 JS 번들에도 포함되지 않죠. 즉, 데이터베이스 쿼리 같은 코드를 브라우저에 보내지 않고도 쓸 수 있다는 겁니다.
npm run dev
혹은 yarn dev
로 구동되는)에서, getStaticProps
는 매 요청마다 작동합니다.getStaticProps
는 빌드 타임에 작동합니다. 하지만, 이는 getStaicPaths
로부터 리턴되는 fallback 키를 사용하여 향상되어질 수 있습니다.빌드타임에 작동하기 때문에 쿼리 파라미터나 HTTP 헤더와 같이 요청 중에만 유효한 데이터는 사용할 수 없습니다.
getStaticProps
는 page에서만 export 될 수 있습니다.
이러한 제한이 존재하는 이유 중 하나는 리액트는 페이지가 렌더링 되기 이전에 모든 필요한 데이터를 가지고 있어야 하기 때문입니다.
만일 페이지를 사용자의 요청 이전에 프리렌더링 할 수 없다면 정적 생성은 좋은 방안이 아닙니다.
만약 당신의 페이지가 자주 업데이트되는 데이터를 보여 주고, 페이지의 컨텐츠가 요청 때마다 바뀐다면, 서버 사이드 렌더링 방식을 사용하거나 프리렌더링을 건너 뛰는 방법도 있습니다. 다음 레슨에서 이와 같은 방법에 대해 얘기해 봅시다.
만약 빌드 타임때가 아니라 요청시에 데이터를 가져와야 한다면, 서버 사이드 렌더링을 시도해 볼 수 있습니다.
서버 사이드 렌더링을 사용하기 위해선, getStaticProps
대신에 getServerSideProps
을 export 해야 합니다.
여기 getServerSideProps
에 대한 스타터 코드가 있습니다. 우리의 블로그 예제에는 필요하지 않은 코드니깐 붙여 넣지는 마세요.
export async function getServerSideProps(context) {
return {
props: {
// props for your component
}
}
}
왜냐하면 getServerSideProps
는 요청 시에 호출되고, 그것의 파라미터(context
)는 요청에 대한 구체적인 변수를 포함하고 있습니다.
getServerSideProps
는 반드시 요청 시에 데이터를 가져와야 하는 페이지를 프리렌더링 할 때에만 사용해야 합니다. 서버 응답 시간(TTFB)은 getStaticProps
에 비해 느릴 수 있습니다. 서버가 모든 요청에 대한 결과를 처리해야 하고, 결과는 별도의 설정이 없는 CDN에 의해선 캐시에 저장될 수도 없기 때문입니다.
만약 데이터를 프리렌더링할 필요 없다면, 다음과 같은 방법(클라이언트 사이드 렌더링)을 사용할 수도 있습니다.
이러한 접근 방식은 유저의 대시보드와 같은 유형의 페이지에 적합합니다. 왜냐하면 대시보드는 사적이고, 사용자 고유의 페이지이고, SEO와 무관하며, 프리렌더링이 필요없는 페이지이기 때문입니다. 데이터 업데이트가 잦아서 요청 시마다 데이터를 가져와야 하기도 하구요.
Next.js를 만든 팀은 SWR이라고 불리는 데이터 페칭(fetching)을 위한 리액트 훅을 만들었습니다. 만약 당신이 클라이언트 사이드에서 데이터를 가져온다면, 이 훅을 사용할 것을 적극 권장합니다.
다음 레슨에선, dynamic routes를 이용해 각각의 블로그 포스트에 대한 페이지를 생성해 볼 거예요.
다시 말하지만,
getStaticProps
와getServerSideProps
에 대한 자세한 정보는 Data Fetching 문서에서 볼 수 있습니다.
우리는 블로그 데이터를 활용해서 메인 페이지를 구현했어요. 하지만 아직 각각의 블로그 페이지는 만들지 못했죠. 우리는 블로그 데이터에 기반한 페이지의 URL을 원합니다. 즉, 우리는 dynamic routes를 사용해야 한다는 거죠.
이 과정에서 당신은 다음과 같은 것을 배우게 될 겁니다.
getStaticPaths
를 사용해서 페이지를 정적으로 생성하는지getStaticProps
를 활용해서 각 블로그 포스트의 데이터를 가져오는지remark
를 활용해서 마크다운을 렌더링하는지이전 과정에서 우리는 페이지 컨텐츠가 외부 데이터에 의존하는 경우에 대해 알아봤어요. 우리는 getStaticProps
를 활용해서 메인 페이지를 렌더링하기 위해 필요한 데이터를 가져왔죠.
이 과정에선 각각의 페이지 path가 외부 데이터에 의존하는 경우에 대해 얘기해 볼 거예요.
Next.js는 외부 데이터에 의존하는 path를 가진 페이지를 정적으로 생성합니다. Next.js의 dynamic URLs 덕분에 가능한 일이죠.
우리는 블로그 포스트에 대한 dynamic routes를 생성하려고 합니다.
posts
라는 상위 폴더 아래에 있는 마크다운 파일들의 이름을 <id>
로 사용하여 각각의 포스트마다 /posts/<id>
형태의 path를 가지길 원해요.ssg-ssr.md
와 pre-rendering.md
가 있기 때문에, /posts/ssg-ssr
그리고 /posts/pre-rendering
이라는 path가 생성되겠죠.다음과 같은 과정을 거치야 합니다. 아직 아무것도 바꾸지 마세요! 다음 장에서 할 거니깐요.
먼저, pages/posts
아래에 [id].js
라는 페이지를 생성하세요. Next.js에서 dynamic routes 페이지는 파일명에 [
와 ]
를 가집니다.
우리는 pages/posts/[id].js
안에 포스트 페이지를 렌더링 해 주는 코드를 작성할 겁니다.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
이제, 새로운 걸 적용해 봅시다. 우리는 이 페이지에서 getStaticPaths
라는 async 함수를 export 할 겁니다. 이 함수에서, 우리는 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
}
마지막으로 id
가 주어진 블로그 포스트에 필요한 데이터를 가져오기 위해 다시 한번 getStaticProps
를 실행시켜야 합니다. 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
}
여기 우리가 얘기하던 내용에 대한 시각적 요약본이 있어요.
다음 페이지에서 실습을 해 봅시다.
먼저, 파일들을 준비해 봅시다.
pages/posts
폴더 안에 [id].js
파일을 생성해 주세요.pages/posts
폴더 안에 있던 first-post.js
파일을 제거해 주세요. 더이상 사용하지 않을 거라서요.그다음, pages/posts/[id].js
파일을 열고, 아래 코드를 붙여 넣어 주세요. ...
이 부분은 나중에 채워 넣을 겁니다.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
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
는 작동하지 않게 됩니다.
마지막으로, 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()
함수에 의해 리턴된 경로의 배열을 가집니다.fallback
은 무시하세요. 나중에 설명할게요.거의 다했습니다. 하지만 아직 getStaticProps
를 실행시키지 않았죠. 다음 페이지에서 해 봅시다!
주어진 id
를 활용해 포스트 페이지를 렌더링하기 위해선 데이터를 가져와야 합니다.
그러기 위해선, 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
}
}
pages/posts/[id].js
파일을 열고, 아래의 코드를
import { getAllPostsIds } from '../../lib/posts'
다음 코드로 치환해 주세요.
import { getAllPostIds, getPostData } from '../../lib/posts'
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
포스트 페이지는 getStaticProps
내부에서 getPostData
함수를 사용하여 포스트 데이터를 가져온 후 props로 리턴합니다.
이제, postData
를 사용하여 Post
컴포넌트를 업데이트 시킵시다. pages/posts/[id].js
에서 export 되는 Post
컴포넌트를 아래의 코드로 바꿔 주세요.
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
끝이에요! 아래의 URL로 접속해 보세요
각 페이지에 해당하는 블로그 데이터를 확인할 수 있습니다.
잘했어요! 우린 성공적으로 dynamic routes를 생성했습니다.
에러가 발생한다면, 파일의 코드가 정확히 되어 있는지 확인해 보세요.
다시 한번, 우리가 했던 것에 대한 시각적 요약본을 확인해 봅시다.
<그림>
아직 마크다운 컨텐츠를 보여주진 못하고 있어요. 다음 장에서 해 볼게요.
마크다운 컨텐츠를 렌더링하기 위해선, remark
라는 라이브러리를 사용해야 합니다. 설치부터 해 보죠.
npm install remark remark-html
그 다음, lib/posts.js
를 열어서 파일 상단에 아래의 코드를 넣어 주세요.
import { remark } from 'remark'
import html from 'remark-html'
그리고 동일 파일 내의 getPostData()
함수를 아래의 remark
를 활용한 코드로 대체해 주세요.
export async function getPostData(id) {
const fullPath = path.join(postsDirectory, `${id}.md`)
const fileContents = fs.readFileSync(fullPath, 'utf8')
// 포스트의 메타데이터 섹션을 파싱하기 위해 gray-matter를 사용
const matterResult = matter(fileContents)
// 마크다운을 HTML 문자열로 변환하기 위해 remarK를 사용
const processedContent = await remark()
.use(html)
.process(matterResult.content)
const contentHtml = processedContent.toString()
// id와 contentHtml 합치기
return {
id,
contentHtml,
...matterResult.data
}
}
중요 :
getPostData
에async
키워드가 들어간 이유는,remark
를 위해await
를 사용해야 하기 때문입니다.async/await
는 데이터를 비동기적으로 가져옵니다.
getPostData
를 호출할 때, await
를 사용하도록 pages/posts/[id].js
안의 getStaticProps
를 업데이트해야 합니다.
export async function getStaticProps({ params }) {
// Add the "await" keyword like this:
const postData = await getPostData(params.id)
// ...
}
마지막으로, dangerouslySetInnerHTML을 사용하는 contentHtml
을 렌더링하기 pages/posts/[id].js
안의 Post
컴포넌트를 업데이트합니다.
아래의 URL로 접속해 보세요
아래처럼 블로그의 컨텐츠가 보여야 합니다.
거의 끝나가네요. 다음 장에서 페이지를 정돈해 봅시다.
포스트 데이터를 활용하여 pages/posts/[id].js
안에 title
태그를 넣어 봅시다. 파일 상단에 next/head
로 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>
)
}
날짜 형식을 변경하기 위해, 우리는 date-fns 라는 라이브러리를 사용할 거예요. 설치부터 해 봅시다.
npm install 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>
}
Note : date-fns의 웹사이트에서
format()
에 대한 다른 스트링 옵션들을 확인할 수 있습니다.
이제, 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로 접속하면, 날짜가 January 1, 2020 형태로 표시되는 걸 볼 수 있습니다.
마지막으로, styles/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 로 접속하면, 페이지가 좀 나아진 게 보일 거예요.
잘했어요! 이제 메인페이지만 다듬으면 끝입니다!
이제 우리의 메인페이지(pages/index.js
)를 정돈해 봅시다. Link
컴포넌트를 사용해서 각 포스트 페이지에 대한 링크를 연결해야 해요.
pages/index.js
를 열고 파일 상단에 Link
와 Date
컴포넌트를 import 해 오는 코드를 추가합니다.
import Link from 'next/link'
import Date from '../components/date'
그런 다음 하단의 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에 대한 유용한 팁을 얘기해 볼게요.
dynamic routes에 관한 필수 정보입니다.
getStaticProps
와 마찬가지로, getStaticPaths
도 어떤 데이터 소스에서든 데이터를 가져올 수 있습니다. 우리의 예제를 본다면, 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
}
}
})
}
getStaticPaths
는 매 요청마다 작동합니다getStaticPaths
는 빌드 타임에 작동합니다.getStaicPaths
에 있던 fallback:false
기억하나요? 무슨 의미일까요?
만약 fallback
이 false
라면, getStaticPaths
에 의해 리턴되지 않은 모든 경로는 404 페이지로 이동합니다.
만약 fallback
이 true
라면, getStaticProps
의 대응 방식은 달라지죠.
getStaticPaths
로부터 리턴된 경로는 빌드 타임때 HTML을 만약 fallback
이 blocking
이라면, 새로운 경로는 getStaticProps
로 서버 사이드 렌더링되고, 이후의 요청을 위해 캐시에 저장됩니다. 그니깐 경로 하나당 딱 한 번만 발생하는 거죠.
이건 우리의 레슨 범위를 넘어선 얘기지만, fallback:true
와 fallback:'blocking'
에 대해 더 알고 싶다면 이 문서를 참조하세요.
Dynamic routes는 꺽쇠([]
)안에 ...
를 추가함으로써 모든 경로를 처리하도록 확장될 수 있습니다. 예를 들면,
pages/posts/[...id].js
는 /posts/a
경로, posts/a/b
경로, posts/a/b/c
경로와도 일치합니다.이 방법을 위해선, getStaticPaths
안에서 id
키의 값들을 반드시 배열 형태로 리턴시켜야 합니다.
return [
{
params: {
// Statically Generates /posts/a/b/c
id: ['a', 'b', 'c']
}
}
//...
]
그리고 getStaticProps
안의 params.id
도 배열이어야 합니다.
export async function getStaticProps({ params }) {
// params.id will be like ['a', 'b', 'c']
}
자세한 내용은 이 문서를 참조하세요.
Next.js 라우터에 접근하고 싶다면, next/router
를 통해 useRouter
훅을 import 해 와야 합니다.
커스텀 된 404 페이지를 만들기 위해선, pages/404.js
파일을 만드세요. 이 파일은 빌드 타임때 정적으로 생성됩니다.
// pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
자세한 내용은 에러페이지에 관한 문서를 참조하세요.
getStaticProps
와 getStaticPaths
에 관한 여러가지 예제들을 만들었습니다. 아래의 코드를 보고 배워 보세요.