[NextJS] Next.JS 공식문서 튜토리얼 번역하며 공부하기 (2)

Jay·2022년 2월 28일
1
post-thumbnail

Next.JS의 공식문서 튜토리얼을 번역합니다
※ 각 챕터 내 Setup 파트는 생략했습니다. 실습하던 프로젝트로 계속 진행하시면 됩니다.
※ 필자 입맛대로 한 번역이므로 주의 요함
※ 오역, 오타에 대한 피드백 두 팔 벌려 환영

Pre-rendering and Data Fetching

우리는 블로그를 만들기 원하지만, 아직까진 블로그 컨텐츠를 등록하지 않았어요. 이번엔 외부의 블로그 데이터를 어떻게 우리의 앱으로 가져오는지에 대해 배워볼 겁니다. 이 튜토리얼에선 블로그 컨텐츠를 파일 시스템 내부에 저장할 거지만, 컨텐츠는 어디에 저장하든 상관 없이 불러와 집니다. (예를 들면, DB나 Headless CMS 같은 곳)

What You'll Learn in This Lesson

이 레슨에서 당신이 배우게 될 것은,

  • Next.js의 프리렌더링 기능
  • 프리렌더링의 두 가지 형태 : 정적 생성서버 사이드 렌더링
  • 데이터가 있는 정적 생성, 데이터 없는 정적 생성
  • getStaticProps를 사용하여 외부 블로그 데이터를 페이지로 import 해 오는 방법
  • getStaticProps에 관한 유용한 정보

1. Pre-rendering

데이터 가져오는 법을 배우기 앞서, Next.js의 중요한 개념인 프리렌더링에 대해 먼저 얘기해 볼게요.

기본적으로 Next.js는 모든 페이지를 프리렌더링합니다. 이 말인 즉슨, Next.js는 클라이언트 사이드의 자바스크립트로 페이지에 대한 전체 HTML을 생성하는 대신에 각 페이지에 대한 HTML을 사전에 생성합니다. 프리렌더링은 퍼포먼스와 SEO의 측면에 있어 더 나은 결과를 가집니다.

생성된 HTML은 해당 페이지에 필요한 최소한의 자바스크립트 코드로 되어있습니다. 페이지가 브라우저에 의해 로드될 때, 페이지의 자바스크립트 코드가 실행되고 페이지를 완벽히 인터렉티브하게 만듭니다. (이 과정을 hydration(수화)라고 합니다.)

Check That Pre-rendering Is Happening

아래의 과정을 거쳐야 프리렌더링이 발생하는 것을 확인할 수 있습니다.

  • 브라우저의 자바스크립트가 작동되지 않게 하고(ex. 크롬)
  • 페이지로 접근합니다.

자바스크립트 없이도 당신의 앱이 렌더링 되는 것을 볼 수 있습니다. 왜냐하면 Next.js는 자바스크립트 없이도 앱의 UI를 볼 수 있도록 하면서 앱을 정적 HTML로 프리렌더링하니깐요.

Summary : Pre-rendering vs No Pre-rendering

Pre-rendering
Pre-rendering (Next.js 사용)

  • Initial Load : 프리렌더링 된 HTML이 보여집니다.
  • Hydration : 리액트 컴포넌트가 초기화되고 앱이 인터렉티브 상태가 됩니다.
  • 만일 당신의 앱이 <Link />와 같은 인터렉티브한 컴포넌트를 가지고 있다면, 해당 요소는 JS가 로드된 후에 활성화됩니다.

No Pre-rendering

No Pre-rendering (기본 리액트 앱)

  • Initial Load : 앱이 렌더링되지 않습니다.
  • Hydration : 리액트 컴포넌트가 초기화되고 앱이 인터렉티브 상태가 됩니다.

다음은 Next.js의 두 가지 프리렌더링 형태를 알아 보겠습니다.

2. Pre-rendering and Data Fetching

Two Forms of Pre-rendering

Next.js는 정적 생성과 서버 사이드 렌더링이라는 두 가지 형태의 프리렌더링을 제공합니다. 이 둘의 차이점은 언제 HTML이 생성되느냐예요.

  • 정적 생성(Static Generation)은 HTML을 빌드 타임에 생성하는 프리렌더링 방식입니다. 프리렌더링 된 HTML은 매 요청마다 재사용됩니다.
  • 서버 사이드 렌더링(Server-Side Rendering)은 HTML이 요청시에 생성되는 프리렌더링 방식입니다.

개발모드에선(npm run dev 혹은 yarn dev 명령어로 실행시켰을 때), 모든 페이지는 요청시에 프리렌더링 됩니다. 정적 생성을 사용하는 페이지에서도 말이죠.

Per-page Basis

중요한 점은, Next.js는 각 페이지에 대한 렌더링 방식을 선택할 수 있도록 한다는 겁니다. 여러분은 대부분의 페이지를 정적 생성 방식으로 렌더링하고 나머지 페이지는 서버 사이드 렌더링 방식으로 렌더링하는 '하이브리드'한 Next.js 앱을 만들 수 있어요.

When to Use Static Generation vs Server-side Rendering

우리는 가능하면 정적 생성 방식을 사용하는 걸 추천합니다. 왜냐하면 당신의 페이지는 CDN이라는 요청 때마다 서버 렌더링 되는 페이지보다 더 빠르게

당신은 다양한 종류의 페이지에 정적 생성 방식을 사용할 수 있습니다. 가령,

  • 마케팅 페이지
  • 블로그 포스팅 페이지
  • 이커머스 상품 목록 페이지
  • 도움말과 문서 페이지

자신에게 물어 보세요. "이 페이지는 유저가 요청하기 전에 미리 프리렌더링 되어야 할까요?" 만약 당신의 대답이 "네"라면, 정적 생성 방식을 선택해야 합니다.

반면에, 유저의 요청보다 먼저 프리렌더링 할 수 없는 페이지의 경우 정적 생성은 좋은 방안이 아닙니다. 당신의 페이지가 꾸준히 업데이트되는 데이터를 보여준다면, 페이지 컨텐츠는 요청 때마다 바뀌어야 합니다.

이런 경우, 서버 사이드 렌더링을 사용합니다. 속도가 보다 느릴 수는 있지만, 프리렌더링 된 페이지는 항상 최신의 상태이죠. 아니면 프리렌더링을 건너뛰고, 자바스크립트를 이용한 클라이언트 사이드 렌더링을 사용해도 됩니다.

We'll Focus on Static Generation

이 레슨에서, 우리는 정적 생성에 중점을 둘 겁니다. 다음 페이지에선 데이터 유무에 따른 정적 생성 방식에 대해 얘기해 볼게요.

3. Static Generation With and without Data

정적 생성은 데이터가 있든 없든 잘 작동합니다.

지금까지 우리가 만든 모든 페이지는 외부 데이터를 가져올 필요가 없었죠. 이런 페이지들은 앱이 배포를 위해 빌드될 때, 자동적으로 정적 생성 방식을 따릅니다.

하지만 어떤 페이지는 외부 데이터 없이는 HTML 렌더링이 불가능할 수도 있습니다. 빌드 타임에 파일 시스템에 접근해야 될 수도, 외부 API 를 가져와야 할 수도, 데이터 베이스 쿼리를 실행해야 할 수도 있겠죠. Next.js는 이러한 경우도 데이터가 있는 정적 생성 방식으로 지원합니다.

Static Generation with Data using 'getStaticProps'

어떻게 그게 가능할까요? Next.js에서 페이지 컴포넌트를 export할 때, 당신은 getStaticProps라는 async 함수도 export 할 수 있습니다.

  • getStaticProps는 배포 단계에서 빌드 타임 때 작동합니다. 그리고..
  • 함수 내에서, 당신은 외부 데이터를 가져올 수 있고, props로 페이지에 보낼 수 있습니다.
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는 요청시에 작동합니다.

Let's Use 'getStaticProps'

실습이 더 배우기 쉽죠. 다음 페이지에선 getStaticProps를 사용해서 우리의 블로그를 만들어 봅시다.

4. Blog Data

이제 파일 시스템을 사용해서 앱에 블로그 데이터를 추가해 볼 겁니다. 각각의 블로그 포스트는 마크다운 파일로 되어 있어요.

  • posts 라는 상위 폴더를 생성하세요.(pages/posts 와는 다른 겁니다.)
  • posts 안에, pre-rendering.mdssg-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.

각각의 마크다운 파일의 윗부분에 titledate를 포함한 메타데이터 섹션이 있다는 걸 눈치챘을 거예요. 이는 YAML Front Matter라고 불리는데, gray-matter라는 라이브러리를 이용해서 파싱할 수 있습니다.

Parsing the Blog Data on 'getStaticProps'

지금부터 데이터를 활용하여 우리의 메인 페이지(pages/index.js)를 업데이트 해 봅시다.

  • 각각의 마크다운 파일을 파싱하여 title, date, 파일명(post URL을 위한 id 값으로 쓰일 겁니다.)을 가져옵니다.
  • 날짜를 기준으로 정렬된 데이터 리스트가 메인 페이지에 실리게 됩니다.

프리렌더링이 이루어지게 하기 위해 getStaticProps를 사용합니다.

다음 페이지에서 해 봅시다!

5. Implement 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.jsgetStaticProps 안에서 호출해야 합니다.

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에 관한 유용한 팁에 대해 얘기해 보겠습니다.

6. getStaticProps Detail

getStaticProps에 대해 꼭 알고 있어야 하는 정보를 알려 드릴게요.

Fetch External API or Query Database

우리는 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 번들에도 포함되지 않죠. 즉, 데이터베이스 쿼리 같은 코드를 브라우저에 보내지 않고도 쓸 수 있다는 겁니다.

Development vs Production

  • 개발단계(npm run dev 혹은 yarn dev로 구동되는)에서, getStaticProps는 매 요청마다 작동합니다.
  • 배포단계에서 getStaticProps는 빌드 타임에 작동합니다. 하지만, 이는 getStaicPaths로부터 리턴되는 fallback 키를 사용하여 향상되어질 수 있습니다.

빌드타임에 작동하기 때문에 쿼리 파라미터나 HTTP 헤더와 같이 요청 중에만 유효한 데이터는 사용할 수 없습니다.

Only Allowed in a Page

getStaticPropspage에서만 export 될 수 있습니다.

이러한 제한이 존재하는 이유 중 하나는 리액트는 페이지가 렌더링 되기 이전에 모든 필요한 데이터를 가지고 있어야 하기 때문입니다.

What If I Need to Fetch Data at Request Time?

만일 페이지를 사용자의 요청 이전에 프리렌더링 할 수 없다면 정적 생성은 좋은 방안이 아닙니다.

만약 당신의 페이지가 자주 업데이트되는 데이터를 보여 주고, 페이지의 컨텐츠가 요청 때마다 바뀐다면, 서버 사이드 렌더링 방식을 사용하거나 프리렌더링을 건너 뛰는 방법도 있습니다. 다음 레슨에서 이와 같은 방법에 대해 얘기해 봅시다.

6. Fetching Data at Request Time

만약 빌드 타임때가 아니라 요청시에 데이터를 가져와야 한다면, 서버 사이드 렌더링을 시도해 볼 수 있습니다.

서버 사이드 렌더링을 사용하기 위해선, getStaticProps 대신에 getServerSideProps을 export 해야 합니다.

Using 'getServerSideProps'

여기 getServerSideProps에 대한 스타터 코드가 있습니다. 우리의 블로그 예제에는 필요하지 않은 코드니깐 붙여 넣지는 마세요.

export async function getServerSideProps(context) {
  return {
    props: {
      // props for your component
    }
  }
}

왜냐하면 getServerSideProps는 요청 시에 호출되고, 그것의 파라미터(context)는 요청에 대한 구체적인 변수를 포함하고 있습니다.

getServerSideProps는 반드시 요청 시에 데이터를 가져와야 하는 페이지를 프리렌더링 할 때에만 사용해야 합니다. 서버 응답 시간(TTFB)은 getStaticProps에 비해 느릴 수 있습니다. 서버가 모든 요청에 대한 결과를 처리해야 하고, 결과는 별도의 설정이 없는 CDN에 의해선 캐시에 저장될 수도 없기 때문입니다.

Cliend-side Rendering

만약 데이터를 프리렌더링할 필요 없다면, 다음과 같은 방법(클라이언트 사이드 렌더링)을 사용할 수도 있습니다.

  • 외부 데이터를 필요로 하지 않는 페이지의 일부분을 정적으로 생생(프리렌더링)합니다.
  • 페이지가 로드될 때, 클라이언트단의 자바스크립트를 사용하여 외부 데이터를 가져오고 나머지 부분을 구현합니다.

이러한 접근 방식은 유저의 대시보드와 같은 유형의 페이지에 적합합니다. 왜냐하면 대시보드는 사적이고, 사용자 고유의 페이지이고, SEO와 무관하며, 프리렌더링이 필요없는 페이지이기 때문입니다. 데이터 업데이트가 잦아서 요청 시마다 데이터를 가져와야 하기도 하구요.

SWR

Next.js를 만든 팀은 SWR이라고 불리는 데이터 페칭(fetching)을 위한 리액트 훅을 만들었습니다. 만약 당신이 클라이언트 사이드에서 데이터를 가져온다면, 이 훅을 사용할 것을 적극 권장합니다.

That's It!

다음 레슨에선, dynamic routes를 이용해 각각의 블로그 포스트에 대한 페이지를 생성해 볼 거예요.

다시 말하지만, getStaticPropsgetServerSideProps에 대한 자세한 정보는 Data Fetching 문서에서 볼 수 있습니다.

Dynamic Routes

우리는 블로그 데이터를 활용해서 메인 페이지를 구현했어요. 하지만 아직 각각의 블로그 페이지는 만들지 못했죠. 우리는 블로그 데이터에 기반한 페이지의 URL을 원합니다. 즉, 우리는 dynamic routes를 사용해야 한다는 거죠.

What You'll Learn in This Lesson

이 과정에서 당신은 다음과 같은 것을 배우게 될 겁니다.

  • 어떻게 dynamic routes와 getStaticPaths를 사용해서 페이지를 정적으로 생성하는지
  • 어떻게 getStaticProps를 활용해서 각 블로그 포스트의 데이터를 가져오는지
  • 어떻게 remark를 활용해서 마크다운을 렌더링하는지
  • 어떻게 날짜 서식을 정돈하는지
  • 어떻게 dynamic routes를 활용해서 페이지를 연결시키는지
  • dynamic routes에 관한 유용한 정보

1. Page Path Depends on External Data

이전 과정에서 우리는 페이지 컨텐츠가 외부 데이터에 의존하는 경우에 대해 알아봤어요. 우리는 getStaticProps를 활용해서 메인 페이지를 렌더링하기 위해 필요한 데이터를 가져왔죠.

이 과정에선 각각의 페이지 path가 외부 데이터에 의존하는 경우에 대해 얘기해 볼 거예요.
Next.js는 외부 데이터에 의존하는 path를 가진 페이지를 정적으로 생성합니다. Next.js의 dynamic URLs 덕분에 가능한 일이죠.

How to Statically Generates Pages with Dynamic Routes

우리는 블로그 포스트에 대한 dynamic routes를 생성하려고 합니다.

  • 우리는 posts라는 상위 폴더 아래에 있는 마크다운 파일들의 이름을 <id>로 사용하여 각각의 포스트마다 /posts/<id> 형태의 path를 가지길 원해요.
  • ssg-ssr.mdpre-rendering.md가 있기 때문에, /posts/ssg-ssr 그리고 /posts/pre-rendering이라는 path가 생성되겠죠.

Overview of the Steps

다음과 같은 과정을 거치야 합니다. 아직 아무것도 바꾸지 마세요! 다음 장에서 할 거니깐요.

먼저, 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를 실행시켜야 합니다. getStaticPropsid를 포함한 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. Implement 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 파일을 열고, 파일 하단에 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
  }
}
  • pathsgetAllPostIds() 함수에 의해 리턴된 경로의 배열을 가집니다.
  • 지금은 false로 되어 있는 fallback은 무시하세요. 나중에 설명할게요.

거의 다했습니다. 하지만 아직 getStaticProps를 실행시키지 않았죠. 다음 페이지에서 해 봅시다!

3. Implement 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를 생성했습니다.

Something Wrong?

에러가 발생한다면, 파일의 코드가 정확히 되어 있는지 확인해 보세요.

  • pages/posts/[id].js다음과 같아야 합니다.
  • lib/posts.js다음과 같아야 합니다
  • (그럼에도 안된다면) 나머지 코드는 다음과 같아야 합니다.

Summary

다시 한번, 우리가 했던 것에 대한 시각적 요약본을 확인해 봅시다.
<그림>

아직 마크다운 컨텐츠를 보여주진 못하고 있어요. 다음 장에서 해 볼게요.

4. Render Markdown

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

중요 : getPostDataasync 키워드가 들어간 이유는, 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로 접속해 보세요

아래처럼 블로그의 컨텐츠가 보여야 합니다.

거의 끝나가네요. 다음 장에서 페이지를 정돈해 봅시다.

5. Polishing the Post Page

Adding 'title' to the Post Page

포스트 데이터를 활용하여 pages/posts/[id].js 안에 title 태그를 넣어 봅시다. 파일 상단에 next/headHead 컴포넌트를 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>
  )
}

Formatting the Date

날짜 형식을 변경하기 위해, 우리는 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 형태로 표시되는 걸 볼 수 있습니다.

Adding CSS

마지막으로, 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 로 접속하면, 페이지가 좀 나아진 게 보일 거예요.

잘했어요! 이제 메인페이지만 다듬으면 끝입니다!

5. Polishing the Index Page

이제 우리의 메인페이지(pages/index.js)를 정돈해 봅시다. Link컴포넌트를 사용해서 각 포스트 페이지에 대한 링크를 연결해야 해요.

pages/index.js를 열고 파일 상단에 LinkDate 컴포넌트를 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에 대한 유용한 팁을 얘기해 볼게요.

6. Dynamic Routes Details

dynamic routes에 관한 필수 정보입니다.

Fetch External API or Query Database

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

Development vs Production

  • 개발 단계에서, getStaticPaths는 매 요청마다 작동합니다
  • 배포 단계에서, getStaticPaths는 빌드 타임에 작동합니다.

Fallback

getStaicPaths에 있던 fallback:false 기억하나요? 무슨 의미일까요?

만약 fallbackfalse라면, getStaticPaths에 의해 리턴되지 않은 모든 경로는 404 페이지로 이동합니다.

만약 fallbacktrue라면, getStaticProps의 대응 방식은 달라지죠.

  • getStaticPaths로부터 리턴된 경로는 빌드 타임때 HTML을
  • 빌드 타임때 생성되지 않은 경로는 404 페이지로 이동하지 않습니다. 대신에, Next.js는 그러한 경로에 대한 첫 번째 요청에 "fallback" 페이지를 제공합니다.
  • Next.js는 요청된 경로를 정적으로 생성할 겁니다. 동일 경로에 대한 차후의 요청은 빌드 타임때 렌더링 된 다른 페이지들처럼 생성되어 있던 페이지를 제공합니다.

만약 fallbackblocking이라면, 새로운 경로는 getStaticProps로 서버 사이드 렌더링되고, 이후의 요청을 위해 캐시에 저장됩니다. 그니깐 경로 하나당 딱 한 번만 발생하는 거죠.

이건 우리의 레슨 범위를 넘어선 얘기지만, fallback:truefallback:'blocking'에 대해 더 알고 싶다면 이 문서를 참조하세요.

Catch-all Routes

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

자세한 내용은 이 문서를 참조하세요.

Router

Next.js 라우터에 접근하고 싶다면, next/router를 통해 useRouter 훅을 import 해 와야 합니다.

404 Pages

커스텀 된 404 페이지를 만들기 위해선, pages/404.js 파일을 만드세요. 이 파일은 빌드 타임때 정적으로 생성됩니다.

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

자세한 내용은 에러페이지에 관한 문서를 참조하세요.

More Examples

getStaticPropsgetStaticPaths에 관한 여러가지 예제들을 만들었습니다. 아래의 코드를 보고 배워 보세요.

profile
개발할래요💻

0개의 댓글