SSR을 하는 이유 -> 어서와, SSR은 처음이지?
Next.js 공식 튜토리얼(en)
Next.js 필요한 것만 빨리 배우기(kr)
Next.js 튜토리얼 따라하기 1탄을 안보셨다면 여기로! 2탄은 여기로!
아래 내용은 next.js 공식 사이트에 있는 learning course를 번역한 것이다.
지난 포스트에서 블로그 데이터를 가져와서 인덱스페이지를 구현했지만 아직 각 포스트 페이지를 만들지는 않았다. 블로그 데이터를 바탕으로 이러한 페이지로 연결되는 URL이 필요한데, 이는 우리가 동적 라우팅을 해야한다는 것을 의미한다.
이전 레슨에서, 외부 데이터에 의존하는 page content를 다뤘다. getStaticProps
를 사용해서 인덱스 페이지에서 요구하는 데이터를 fetch 했다. 이번에는, 외부 데이터에 의존하는 각 page path에 대해 이야기해보자. Next.js는 외부 데이터에 의존하는 path를 가지고 정적 페이지를 생성하는데 이를 dynamic URL이라고 한다.
블로그 포스트를 위한 동적 라우트를 생성하고자 한다.
/post/<id>
형태의 path를 갖게 한다. <id>
는 posts
디렉토리 아래에 있는 마크다운 파일의 이름이다.ssg-ssr.md
와 pre-rendering.md
파일이 있으니, 각 path는 /posts/ssg-ssr
과 /posts/pre-rendering
이 된다.아래의 순서를 따라하면 된다. 아직 실제 파일에 반영할 필요는 없다!
pages/posts
아래에 [id].js
라는 이름의 페이지를 생성한다.[
와 ]
형태로 이루어진 페이지가 Next.js에서 동적라우팅이 된다. pages/posts/[id].js
에는 포스트 페이지를 렌더할 코드를 적어주면 된다.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
getStaticPaths
라고 불리는 async function을 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
}
getStaticProps
를 다시 추가한다.이 경우, 주어진 id
의 블로그 포스트에 필요한 데이터를 fetch 해오기 위해서이다. 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
}
getStaticPaths
삽입하기파일을 셋업한다.
[id].js
라는 이름의 파일을 pages/posts
디렉토리 안에 만든다.pages/posts
디렉토리 안의 first-post.js
파일을 지운다. - 더 이상 사용하지 않는다.그러고 나서 pages/post/[id].js
파일을 열고 아래 코드를 넣는다. ...
부분은 나중에 수정한다.
import Layout from '../../components/layout'
export default function Post() {
return <Layout>...</Layout>
}
lib/post.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$/, '')
}
}
})
}
중요! 🔥
반환되는 리스트는 단순한 string으로 이루어진 배열이 아니라, 위 주석과 같이 객체로 이루어진 배열이어야 한다. 각 객체는 반드시params
이름의 키를 갖고,id
키를 가진 오브젝트를 포함해야 한다. (파일 이름이[id]
이기 때문이다.) 이 조건을 충족하지 않는다면getStaticPaths
를 사용할 수 없다.
마지막으로, getAllPostIds
함수를 getStaticPaths
에서 호출한다. pages/posts/[id].js
를 열고 Post
컴포넌트 위에 아래의 코드를 붙여넣기 하자.
import { getAllPostIds } from '../../lib/posts'
export async function getStaticPaths() {
const paths = getAllPostIds()
return {
paths,
fallback: false
}
}
paths
는 getAllPostIds()
로부터 리턴된 paths로 이루어진 배열을 포함한다. 이는 pages/posts/[id].js
로 정의된 params를 포함한다.fallback: false
는 나중에 알아보자.getStaticProps
삽입하기포스트를 렌더하기 위해서 주어진 id
를 가지고 필요한 데이터를 fetch 해야 한다. 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 { getAllPostIds } from '../../lib/posts'
이 코드로 대체한다.
import { getAllPostIds, getPostData } from '../../lib/posts'
export async function getStaticProps({ params }) {
const postData = getPostData(params.id)
return {
props: {
postData
}
}
}
이제 post page는 getStaticProps
함수 안에서 getPostData
를 사용하여 필요한 데이터를 가져오고 이를 props로 리턴하게 된다.
이제 postData
를 사용하여 Post
컴포넌트를 수정하면 된다. pages/posts/[id].js
안의 Post
컴포넌트를 아래와 같이 수정한다.
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
</Layout>
)
}
여기까지 했다면, 각 페이지에서 블로그 데이터를 확인할 수 있다.
이렇게 동적 라우팅을 성공했다! 이제 블로그에 마크다운 컨텐츠를 추가해보자.
마크다운 컨텐츠를 렌더하기 위해서는 remark
라는 라이브러리를 사용해야 한다. 일단 설치하자.
npm install remark remark-html
그리고 나서, lib/post.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
}
}
중요! 🔥
remark
에await
를 사용해야 하기 때문에getPostData
에async
키워드를 추가하는 것을 잊지 말자!
이는 pages/posts/[id].js
의 getStaticProps
에서도 getPostData
를 호출할때 await
를 사용해야 함을 의미한다.
export async function getStaticProps({ params }) {
// Add the "await" keyword like this:
const postData = await getPostData(params.id)
// ...
}
마지막으로, Post
컴포넌트에서도 contentHtml
을 렌더하도록 수정하자.
export default function Post({ postData }) {
return (
<Layout>
{postData.title}
<br />
{postData.id}
<br />
{postData.date}
<br />
<div dangerouslySetInnerHTML={{ __html: postData.contentHtml }} />
</Layout>
)
}
다시 각 페이지를 방문해보면, 블로그 컨텐츠가 추가된 것을 볼 수 있다.
next/head
를 사용하여 메타데이터를 추가하거나 날짜 포맷을 바꾸는 등의 부수적인 내용으로 링크로 대체합니다.
Polishing the Post Page
https://nextjs.org/learn/basics/dynamic-routes/polishing-post-page
Polishing the Index Page
https://nextjs.org/learn/basics/dynamic-routes/polishing-index-page
getStaticProps
, getStaticPaths
가 어떤 데이터 소스로부터 데이터를 fetch 해오듯이, getAllPostIds
함수(getStaticPaths
에서 호출되어 사용된)도 외부 API 엔드포인트에 fetch 할 수 있다.
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
는 매 리퀘스트마다 실행한다 (npm run dev
, yarn dev
)getStaticPaths
는 빌드 타임에 실행한다.getStaticPaths
에서 리턴했던 fallback: false
는 무엇을 의미할까?
fallback
이 false
면, getStaticPaths
로 리턴되지 않는 paths는 404 페이지를 리턴한다.
fallback
이 true
인 경우, getStaticProps
의 동작 방식이 달라진다.
getStaticPaths
에서 반환된 경로는 반드시 빌드 시에 HTML로 렌더링된다.fallback
버전을 제공한다.fallback
이 blocking
이라면, 새로운 경로는 getStaticProps
로 서버사이드 렌더링되고, 이후에 있을 요청을 위해 캐시된다. 따라서 하나의 경로당 한번만 렌더링이 일어난다.
동적 라우트는 3개의 dots(...
)를 추가하여 모든 경로를 포착하도록 확장할 수 있다. 예를 들어, 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']
}
}
//...
]
그리고 params.id
는 getStaticProps
에서 배열로 넘겨진다.
export async function getStaticProps({ params }) {
// params.id will be like ['a', 'b', 'c']
}
만약 Next.js router에 접근하고 싶다면, useRouter
훅을 next/router
에서 import하면 된다.
커스텀 404 페이지를 만들고 싶다면 pages/404.js
파일을 만들면 된다. 이 파일은 빌드 타임에 정적으로 생성된다.
// pages/404.js
export default function Custom404() {
return <h1>404 - Page Not Found</h1>
}
API 라우트는 Next.js 앱 안에서 API 엔드포인트를 만들 수 있게 한다. 아래의 포맷대로 pages/api
디렉토리 안에 함수를 만들면 된다.
// req = HTTP incoming message, res = HTTP server response
export default function handler(req, res) {
// ...
}
request handeler에 대해 더 알아보고 싶다면 API Routes 공식문서를 참고하자
serverless Function으로 배포가 가능하다 (lambdas) -> 뭔지 아직 모름..
pages/api
안에 hello.js
파일을 만들고 아래의 코드를 추가한다.
export default function handler(req, res) {
res.status(200).json({ text: 'Hello' })
}
링크에 접속하면 {"text":"Hello"}
를 확인할 수 있다.
getStaticProps
나 getStaticPaths
에서 API 라우트를 fetch 하지 말 것getStaticProps
나 getStaticPaths
에서는 API Route를 가져오지 않아야 한다. 대신 그 안에서 직접 server-side 코드를 작성해야 한다. getStaticProps
나 getStaticPaths
는 오직 server-side에서만 동작한다. client-side에서는 실행되지 않는다. 브라우저용 JS 번들에도 포함되지 않는다. 즉, 직접 데이터베이스 쿼리와 같은 코드를 브라우저로 보내지 않고도 작성할 수 있다.
form input을 처리하는 것은 API Routes를 사용하는 좋은 방법이다. 예를 들어, 페이지에서 만든 form에서 API Route로 POST
요청을 보낼 수 있다. 그런 다음 코드를 작성하여 데이터베이스에 직접 저장할 수 있다. API Route 코드는 클라이언트 번들의 일부가 아니므로 server-side code를 안전하게 작성할 수 있다.
export default (req, res) => {
const email = req.body.email
// Then save email to your database, etc...
}
Static Generatioon은 headless CMS에서 데이터를 가져올 때 유용하다. 그러나 headless CMS에서 초안을 작성하고 페이지에서 즉시 그 초안을 미리보고 싶을 경우에는 적합하지 않다. Next.js가 build time이 아닌 request time에 이러한 페이지를 렌더링하고, 게시된 콘텐츠 대신 초안 콘텐츠를 가져오기를 원하는 경우라면, 이런 특정 상황에만 Next.js가 정적 생성을 우회하기를 원할 것이다.
Next.js에는 위의 문제를 해결하기 위한 Preview Mode라는 기능이 있으며 API Route를 활용한다.
더 알아보고 싶다면 Preview Mode 공식문서를 참고하자
API Routes도 다른 페이지처럼 동적 형태가 가능하다. Dynamic API Routes 공식문서를 참고하자.