[번역] Next.js 시작하기 5. 동적 라우트

여름노래불러줘·2020년 5월 21일
0

1.

블로그 데이터로 인덱스 페이지를 채웠습니다. 그렇지만, 개인 블로그 페이지를 만들지는 않았습니다. (우리가 원하는 결과).
블로그 데이터에 따라 이 페이지들에 URL 을 주고 싶습니다. 동적 라우트가 필요하다는 뜻입니다.

이번 레슨에서 배울 것들

이번 레슨에서 배울것들:

  • 'getStaticPaths'를 사용해 동적 라우트로 페이지를 정적으로 생성하는 방법.
  • 각 블로그 포스트들에 데이터를 가져오기 위해 'getStaticProps' 를 작성하는 방법.
  • 'remkar' 를 사용해 마크다운을 렌더하는 방법.
  • 날짜 문자열을 Pretty-print 하는 방법.
  • 페이지에 동적 라우트로 링크를 거는 방법.
  • 동적 라우트에 대한 유용한 정보들

2.

이전 레슨부터 계속하고 있다면, 이 페이지를 건너뛰어도 됩니다. 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]

에 당신의 자기소개.

3.

외부 데이터에 따른 페이지 경로

이전 레슨에서, 우리는 외부 데이터에 따른 페이지 내용을 다루었습니다. 요청된 데이터를 가져와 인덱스 페이지를 렌더링하기 위해 'getStaticProps' 를 사용했었죠.

이 레슨에선, 각 페이지 경로가 외부 데이터에 의존하는 경우에 대해서 이야기 할 것입니다. Next.js 는 외부 데이터로 경로 및 페이지를 정적으로 생성하게 해줍니다. 그러면 Next.js 에서 동적 URL 을 사용할 수 있습니다.

정적 페이지 생성 및 동적으로 라우팅하는 방법

우리의 경우에서는, 블로그 포스트를 위해 동적으로 페이지를 생성하고 싶습니다:

  • 각 포스트가 '/posts/<id>' 의 경로를 갖게 하고 싶습니다. '<id>' 는 최상위 'posts' 디렉토리 밑의 마크다운 파일의 이름입니다.
  • 'ssg-ssr.md' 와 'pre-rendering.md' 파일을 가지고 있으니, '/posts/ssg-ssr' 과 '/posts/pre-rendering' 의 경로를 가지게 하고 싶습니다.

구분동작

아래의 단계를 거치면 우리도 할 수 있습니다. 아직 이 작업을 따라 할 필요는 없습니다 — 다음 페이지에서 다 할 거에요.

첫 째, '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
}

무슨 얘기 한건지 그림으로 요약:

다음 페이지에서 동적 라우팅을 시도해보죠!

4.

getStaticPaths 구현

먼저, 파일들을 세팅합시다:

  • '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' 를 열고 이 함수를 추가하세요. 이 함수는 '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
  }
}
  • 'id' 를 위한 배열의 가능한 값은 반드시 리턴된 객체의 키의 'paths' 의 값이어야 합니다. 이것이 바로 'getAllPostIds()' 가 리턴하는 것입니다.
  • 'fallback: false' 는 무시하세요, — 나중에 설명해드릴게요.

거의 다 했습니다 — 그러나 여전히 'getStaticProps' 를 구현해야 합니다. 다음 페이지에서 해볼까요?

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

이게 전부입니다! 이 페이지들을 방문해보세요:

잘했습니다! 성공적으로 동적 페이지를 생성했습니다.

뭔가 잘못되었나요?

에러와 싸우고 있다면, 파일들이 맞는 코드를 가지고 있는지 확인해보세요:

  • 'pages/posts/[id].js' 는 이렇게 보여야 합니다.
  • 'lib/posts.js' 는 이렇게 보여야 합니다.
  • (그래도 여전히 안되면) 나머지 코드가 이렇게 보여야 합니다.

여전히 삽질 중이라면, GitHub Discussions 커뮤니티에 자유롭게 질문 하세요. 당신의 코드를 GitHub 에 푸쉬하고 다른 사람들이 볼 수 있게 링크할 수 있다면 도움이 될 것입니다.

요약

우리가 한 일을 또 그림으로 요약했어요:

그러나 우리는 여전히 블로그 마크다운 컨텐츠를 화면에 표시하지 않았습니다. 다음에 해보죠.

6.

마크다운 렌더링하기

마크다운 컨텐츠를 렌더링하려고 '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>
  )
}

다시 한번 이 페이지들을 방문해보세요:

이런 블로그 컨텐츠가 보여야 합니다:

거의 다했어요! 페이지를 좀 더 다듬어보죠

7.

포스트 페이지 가다듬기

포스트 페이지에 'title' 추가하기

'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" 이라고 적힌것이 보여야 합니다.

CSS추가하기

마지막으로, '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 에 접속하면 페이지가 조금 더 나아 보일 겁니다:

잘했어요! 인덱스 페이지를 가다듬고 끝내죠!

8.

인덱스 페이지 가다듬기

마지막 단계로, 인덱스 페이지를 업데이트 해봅시다. ('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>

이제, 각 아티클들은 링크를 가져야 합니다.

만약 뭔가 안된다면, 당신의 코드가 이렇게 보이는지 확인해보세요.

이게 전부입니다! 이 레슨을 마무리하기 전, 다음 페이지에서 동적 라우팅에 대한 몇가지 팁에 대해 얘기 해봅시다.

9.

동적 라우트 세부 사항

우리 도큐먼테이션에서 동적 라우트에 대한 자세한 정보를 얻을 수 있습니다:

그러나 당신이 알아야할 동적 라우팅에 대한 본질적인 정보들이 있습니다.

외부 API 가져오기나 데이터베이스에 쿼리 날리기

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

개발 v.s 프로덕션

  • 개발모드에서 ('npm run dev' 나 'yarn dev'), 'getStaticPaths' 는 매 요청마다 실행 됩니다.
  • 프로덕션에서, 'getStaticPaths' 는 빌드시 실행 됩니다.

Fallback

'getStaticPaths'로 부터 'fallback: false'를 리턴했던것을 떠올려보세요. 이게 무슨 뜻일까요?

'fallback'이 false 면, 'getStaticPaths' 에서 리턴되지 않은 경로는 404 페이지가 됩니다.

'fallback' 이 true 면, 'getStaticProps' 의 행동이 변합니다:

  • 'getStaticPaths' 로 부터 리턴 된 경로는 빌드 시 HTML로 렌더링 될 것입니다.

  • 빌드 시 생성되지 않은 페이지들은 404 페이지가 되지 않습니다. 대신, Next.js 는 이런 경로에 대한 첫 번째 요청에서 페이지의 "fallback" 버전을 제공할 것입니다.

  • 백그라운드에서, Next.js 가 요청된 경로를 정적으로 생성합니다. 같은 경로에 들어온 후속요청은 그 생성된 페이지가 제공됩니다. 빌드 시 사전-렌더링 된 다른 페이지들처럼 말이죠.

이것은 우리 레슨의 범위의 너머입니다.하지만, 도큐먼테이션에서 'fallback: true' 에 대해 더 학습해 볼 수 있습니다.

포괄-라우트

동적 라우트는 괄호 안에 세 개의 점('...')을 추가하는 것으로 모든 경로를 잡게 확장될 수 있습니다. 예를 들면:

  • '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']
}

동적 라우트에 대한 도큐먼테이션을 보고 더 학습하세요.

라우터

'useRouter' hook 를 'next/router' 에서 가져오는 것으로 Next.js 라우터에 접근할 수 있습니다. 라우터 도큐먼테이션을 보고 더 학습하세요.

404 페이지들

커스텀 404 페이지를 만들려면, 'pages/404.js' 를 생성하세요. 이 파일은 빌드 시 정적으로 생성됩니다.

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

에러 페이지에 대한 도큐먼테이션을 보고 더 학습 하세요.

더 많은 예제들

'getStaticProps' 와 'getStaticPaths' 를 설명하기 위해 몇가지 예제를 만들었습니다. — 소스코드를 보고 더 학습하세요.

이게 전부입니다!

다음 레슨에서, Next.js 의 API 라우터 기능에 대해 얘기 해 봅시다.

0개의 댓글