TIL 94 | Next.js 동적라우팅과 API 라우팅 방법

hyounglee·2020년 12월 30일
0

React

목록 보기
33/33

SSR을 하는 이유 -> 어서와, SSR은 처음이지?
Next.js 공식 튜토리얼(en)
Next.js 필요한 것만 빨리 배우기(kr)

Next.js 튜토리얼 따라하기 1탄을 안보셨다면 여기로! 2탄은 여기로!

아래 내용은 next.js 공식 사이트에 있는 learning course를 번역한 것이다.

Dynamic Routes

지난 포스트에서 블로그 데이터를 가져와서 인덱스페이지를 구현했지만 아직 각 포스트 페이지를 만들지는 않았다. 블로그 데이터를 바탕으로 이러한 페이지로 연결되는 URL이 필요한데, 이는 우리가 동적 라우팅을 해야한다는 것을 의미한다.

1. 외부 데이터에 의존하는 Page Path

이전 레슨에서, 외부 데이터에 의존하는 page content를 다뤘다. getStaticProps를 사용해서 인덱스 페이지에서 요구하는 데이터를 fetch 했다. 이번에는, 외부 데이터에 의존하는 각 page path에 대해 이야기해보자. Next.js는 외부 데이터에 의존하는 path를 가지고 정적 페이지를 생성하는데 이를 dynamic URL이라고 한다.

동적 라우트로 정적 페이지를 생성하기

블로그 포스트를 위한 동적 라우트를 생성하고자 한다.

  • 각 포스트가 /post/<id> 형태의 path를 갖게 한다. <id>posts 디렉토리 아래에 있는 마크다운 파일의 이름이다.
  • ssg-ssr.mdpre-rendering.md 파일이 있으니, 각 path는 /posts/ssg-ssr/posts/pre-rendering이 된다.

간략하게 훑어보기

아래의 순서를 따라하면 된다. 아직 실제 파일에 반영할 필요는 없다!

1. pages/posts 아래에 [id].js라는 이름의 페이지를 생성한다.

[] 형태로 이루어진 페이지가 Next.js에서 동적라우팅이 된다. pages/posts/[id].js에는 포스트 페이지를 렌더할 코드를 적어주면 된다.

import Layout from '../../components/layout'

export default function Post() {
  return <Layout>...</Layout>
}

2. 위 페이지에 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
}

3. 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
}

2. 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
  }
}
  • pathsgetAllPostIds()로부터 리턴된 paths로 이루어진 배열을 포함한다. 이는 pages/posts/[id].js로 정의된 params를 포함한다.
  • fallback: false는 나중에 알아보자.

3. 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>
  )
}

여기까지 했다면, 각 페이지에서 블로그 데이터를 확인할 수 있다.

  • http://localhost:3000/posts/ssg-ssr
  • http://localhost:3000/posts/pre-rendering

이렇게 동적 라우팅을 성공했다! 이제 블로그에 마크다운 컨텐츠를 추가해보자.

4. 마크다운 렌더하기

마크다운 컨텐츠를 렌더하기 위해서는 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
  }
}

**중요! 🔥 **
remarkawait를 사용해야 하기 때문에 getPostDataasync 키워드를 추가하는 것을 잊지 말자!

이는 pages/posts/[id].jsgetStaticProps에서도 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>
  )
}
  • http://localhost:3000/posts/ssg-ssr
  • http://localhost:3000/posts/pre-rendering

다시 각 페이지를 방문해보면, 블로그 컨텐츠가 추가된 것을 볼 수 있다.

5. Post/Index 페이지 꾸미기

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

6. 동적 라우팅 세부사항

외부 API나 쿼리 데이터베이스 fetch하기

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
      }
    }
  })
}

개발 vs 운영

  • 개발환경에서, getStaticPaths는 매 리퀘스트마다 실행한다 (npm run dev, yarn dev)
  • 운영환경에서 getStaticPaths는 빌드 타임에 실행한다.

Fallback

getStaticPaths에서 리턴했던 fallback: false는 무엇을 의미할까?

fallbackfalse면, getStaticPaths로 리턴되지 않는 paths는 404 페이지를 리턴한다.

fallbacktrue인 경우, getStaticProps의 동작 방식이 달라진다.

  • getStaticPaths에서 반환된 경로는 반드시 빌드 시에 HTML로 렌더링된다.
  • 빌드시 생성되지 않은 경로는 404 페이지가 되지 않는다. 대신 Next.js는 이러한 경로에 대한 첫 번째 요청에서 페이지의 fallback 버전을 제공한다.
  • background에서 Next.js는 요청된 경로를 정적으로 생성한다. 동일한 경로에 대한 후속 요청은 빌드시 사전 렌더링 된 다른 페이지와 마찬가지로 생성된 페이지를 제공한다.

fallbackblocking이라면, 새로운 경로는 getStaticProps로 서버사이드 렌더링되고, 이후에 있을 요청을 위해 캐시된다. 따라서 하나의 경로당 한번만 렌더링이 일어난다.

fallback에 대해 더 알고 싶다면

Catch-all Routes

동적 라우트는 3개의 dots(...)를 추가하여 모든 경로를 포착하도록 확장할 수 있다. 예를 들어, pages/posts/[...id].jsposts/a 뿐만 아니라 posts/a/b, posts/a/b/c와도 매치된다.

이를 getStaticPaths에서 사용하고 싶다면 반환되는 배열의 id 키의 값만 아래와 같이 작성하면 된다.

return [
  {
    params: {
      // Statically Generates /posts/a/b/c
      id: ['a', 'b', 'c']
    }
  }
  //...
]

그리고 params.idgetStaticProps에서 배열로 넘겨진다.

export async function getStaticProps({ params }) {
  // params.id will be like ['a', 'b', 'c']
}

Router

만약 Next.js router에 접근하고 싶다면, useRouter 훅을 next/router에서 import하면 된다.

404 Pages

커스텀 404 페이지를 만들고 싶다면 pages/404.js 파일을 만들면 된다. 이 파일은 빌드 타임에 정적으로 생성된다.

// pages/404.js
export default function Custom404() {
  return <h1>404 - Page Not Found</h1>
}

API Routes

1. API 라우트 생성하기

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) -> 뭔지 아직 모름..

간단하게 API endpoint 만들기

pages/api안에 hello.js 파일을 만들고 아래의 코드를 추가한다.

export default function handler(req, res) {
  res.status(200).json({ text: 'Hello' })
}
  • http://localhost:3000/api/hello

링크에 접속하면 {"text":"Hello"}를 확인할 수 있다.

2. API 라우트 세부사항

getStaticPropsgetStaticPaths에서 API 라우트를 fetch 하지 말 것

getStaticPropsgetStaticPaths에서는 API Route를 가져오지 않아야 한다. 대신 그 안에서 직접 server-side 코드를 작성해야 한다. getStaticPropsgetStaticPaths는 오직 server-side에서만 동작한다. client-side에서는 실행되지 않는다. 브라우저용 JS 번들에도 포함되지 않는다. 즉, 직접 데이터베이스 쿼리와 같은 코드를 브라우저로 보내지 않고도 작성할 수 있다.

좋은 사례: Handling Form Input

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...
}

Preview Mode (100% 이해는 못함)

Static Generatioon은 headless CMS에서 데이터를 가져올 때 유용하다. 그러나 headless CMS에서 초안을 작성하고 페이지에서 즉시 그 초안을 미리보고 싶을 경우에는 적합하지 않다. Next.js가 build time이 아닌 request time에 이러한 페이지를 렌더링하고, 게시된 콘텐츠 대신 초안 콘텐츠를 가져오기를 원하는 경우라면, 이런 특정 상황에만 Next.js가 정적 생성을 우회하기를 원할 것이다.

Next.js에는 위의 문제를 해결하기 위한 Preview Mode라는 기능이 있으며 API Route를 활용한다.

더 알아보고 싶다면 Preview Mode 공식문서를 참고하자

동적 API Routes

API Routes도 다른 페이지처럼 동적 형태가 가능하다. Dynamic API Routes 공식문서를 참고하자.

profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

0개의 댓글