TIL 93 | Next.js의 Pre-rendering과 Data Fetch 방법

hyounglee·2020년 12월 21일
1

React

목록 보기
32/33
post-thumbnail

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

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

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

Pre-rendering and Data Fetching

Pre-rendering

Pre-rendering은 Next.js에서 중요한 컨셉 중 하나다. 기본적으로, Next.js는 모든 페이지를 pre-render한다. 이는 Next.js가 client-side JavaScript로 모든 작업을 수행하는 대신 미리 각 페이지에 대해서 HTML을 만들어두는 것을 의미한다. Pre-rendering은 SEO에서 더 나은 퍼포먼스를 보여줄 수 있다.

이렇게 만들어진 HTML은 해당 페이지에 적은 자바스크립트 코드와 연관된다. 페이지가 브라우저에 로드될 때, 해당 자바스크립트 코드가 작동하고 완전한 인터렉티브 페이지가 되도록 만든다. 이를 Hydration이라고 한다.

Check That Pre-rendering Is Happening

브라우저에서 JavaScript를 disable 하고 앱을 실행하면 앱의 UI가 자바스크립트가 적용되지 않은 채 정적인 HTML로 나타나는 것을 확인할 수 있다.

Pre-rendering vs No Pre-rendering

Two Forms of Pre-rendering

Next.js의 Pre-rendering에는 두가지 종류가 있다. Static Generation과 Server-side Rendering. 이 둘의 차이는 언제 페이지를 위한 HTML을 만드는지에 있다.

  • Static Generation은 HTML을 빌드 타임에 생성한다. pre-render된 HTML은 그 다음에 각 리퀘스트에서 재사용된다.
  • Server-side Rendering은 HTML을 각 리퀘스트가 일어날 때 생성하는 방식이다.

개발 모드(development mode)에서는 Static Generation을 사용하는 페이지라도 모든 페이지가 각 리퀘스트에 pre-render된다.

Per-page Basis

Next.js에서는 각 페이지에 어떤 종류의 pre-rendering을 할지를 선택할 수 있다.

When to Use Static Generation vs Server-side Rendering

가능한 Static Generation을 추천한다. 왜냐면 페이지가 한번에 빌드 될 수 있고, 각 요청에 따라 페이지를 렌더하는데 속도가 빨라지기 때문이다. 유저의 요청보다 먼저 해당 페이지를 렌더할 수 있는가 질문했을때 대답이 그렇다면 Static Generation을 사용해야 한다.

반대로 유저의 요청보다 먼저 페이지가 렌더되는 것이 좋지 않다면 Static Generation을 사용하지 않는 것이 좋다. 자주 데이터를 업데이트 한다거나, 매 요청마다 콘텐츠가 달라지는 경우가 될 것이다. 이런 경우 Server-side Rendering을 사용해야 한다. 렌더 시간은 좀 더 걸리겠지만, 페이지는 항상 업데이트되어있을 것이다. 아니면 그냥 자주 업데이트되는 데이터를 사용하는 경우 pre-rendering을 하지 않고 client-side에서 자바스크립트를 사용하는 것도 방법이 될 수 있다.

Static Generation w/ & w/o Data

Static Generation은 데이터가 있어도, 없어도 가능하다. 이때까지 실습한 페이지들은 외부 데이터를 fetch하지 않은 경우였다. 이런 페이지들은 앱이 빌드될 때 자동으로 정적으로 생성된다.

그러나, 어떤 페이지들은 처음에 외부 데이터를 fetch해오지 않으면 HTML을 렌더하는게 불가능하기도 하다. 이런 경우 파일 시스템에 접근하거나, 외부 API를 fetch하거나, 또는 빌드타임에 DB에 접근해야할 수도 있다. Next.js는 이런 경우를 지원한다.

Static Generation with Data using getStaticProps

Next.js에서 페이지 컴포넌트를 내보낼때, getStaticProps라는 async 함수도 같이 내보낼 수 있다. 이렇게 한다면...

  • 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에게 이렇게 전달한다. "야, 이 페이지는 데이터 의존성이 있어. 그러니까 이 페이지 빌드 타임에 pre-render할 때, 그거부터 먼저 해결해!"

개발 모드에서 getStaticProps는 각 요청에 따라 작동한다.

Blog Data

실습중인 파일 시스템에 블로그 데이터를 추가해보자. 각 블로그 포스트는 마크다운 형식으로 되어있다.

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

각 마크다운 파일에는 윗 섹션에 titledate라는 이름의 메타데이터가 있다. 이는 YAML Front Matter라고 불리고, gray-matter라는 라이브러리로 파싱하여 사용할 수 있다.

Parsing the Blog Data on getStaticProps

위 데이터를 사용하여 인덱스 페이지를 업데이트 해보자. 마크다운 파일에서 title, date, 그리고 파일명(post URL에 사용할 id)를 가져오고, 인덱스 페이지에 date로 소팅하여 리스트로 보여주면 된다.

이것들을 pre-render 하기 위해서는 getStaticProps를 추가해주면 된다.

Implement getStatic Props

먼저 마크다운 파일의 메타데이터를 파싱하기 위해서 gray-matter를 설치한다.

npm install gray-matter

그리고 나서 파일 시스템으로부터 데이터를 fetch하는 간단한 라이브러리를 만들어보자.

  • lib라는 이름의 최상위 디렉토리를 만든다.
  • lib안에 다음의 내용을 담은 post.js 파일을 생성한다.
import fs from 'fs'
import path from 'path'
import matter from 'gray-matter'

const postsDirectory = path.join(process.cwd(), 'posts')

export function getSortedPostsData() {
  // Get file names under /posts
  const fileNames = fs.readdirSync(postsDirectory)
  const allPostsData = fileNames.map(fileName => {
    // Remove ".md" from file name to get id
    const id = fileName.replace(/\.md$/, '')

    // Read markdown file as string
    const fullPath = path.join(postsDirectory, fileName)
    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
    }
  })
  // Sort posts by date
  return allPostsData.sort((a, b) => {
    if (a.date < b.date) {
      return 1
    } else {
      return -1
    }
  })
}

pages/index.jsgetSortedPostData를 import하고, getStaticProps안에서 불러낸다.

import { getSortedPostsData } from '../lib/posts'

export async function getStaticProps() {
  const allPostsData = getSortedPostsData()
  return {
    props: {
      allPostsData
    }
  }
}

getStaticPropsprops객체 안에 있는 allPostData를 리턴함으로써 포스트가 Home 컴포넌트에 prop으로서 전달된다. 아래와 같은 방식으로 블로그 포스트에 접근할 수 있다.

export default function Home ({ allPostsData }) { ... }

블로그 포스트를 보여주기 위해서, Home 컴포넌트의 자기소개 섹션 아래에 또 다른 <section> 태그와 데이터를 추가한다. props에 allPostData를 추가하는걸 잊지 말자!

export default function Home({ allPostsData }) {
  return (
    <Layout home>
      {/* Keep the existing code here */}

      {/* Add this <section> tag below the existing <section> tag */}
      <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>
  )
}

로컬호스트에서 블로그 데이터를 확인할 수 있다. 이렇게 외부 데이터(파일 시스템에서)를 fetch해오고, 그 데이터로 인덱스 페이지를 pre-render 하는데 성공했다!

getStaticProps Details

getStaticProps에 관한 필수 정보는 여기서 확인할 수 있다.

Fetch External API or Query Database

lib/post.js안에, 파일 시스템으로부터 데이터를 받아오는 getSortedPostsData를 추가했다. 하지만, 외부 API 엔드포인트와 같이 다른 소스로부터 데이터를 fetch 해올 수도 있다.

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 polyfills fetch() on both the client and server. You don't need to import it.

데이터베이스에 바로 쿼리를 보낼 수도 있다.

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 번들에 포함되지도 않는다. 이는 브라우저에 전송할 필요 없이 직접적인 database 쿼리와 같은 코드를 작성할 수 있음을 의미한다.

Development vs. Production

  • Development( npm run dev / yarn dev ): getStaticProps는 매 리퀘스트마다 돌아간다.
  • Production: getStaticProps는 빌드타임에 돌아간다. 하지만, 이 점은 getStaticPaths에서 리턴되는 fallback 키로 향상될 수 있다.

-> 뭔소린지 이해 불가... 추가학습 필요 🔥

빌드 타임에 돌아가도록 되어있기 때문에, 쿼리 파라미터나 HTTP 헤더와 같이 리퀘스트가 일어났을 때만 가능한 데이터는 사용할 수 없다.

Only Allowed in a Page

getStaticPropspage에서만 export 될 수 있다. non-page file에서는 export 될 수 없다. 이러한 제한을 둔 이유는, 리액트에서 페이지가 렌더되기 전에 모든 필요한 데이터를 가지고 있어야 하기 때문이다.

Next.js에서 page는 pages 디렉토리에서 .js, .jsx, .ts, .tsx 파일에서 추출된 리액트 컴포넌트를 의미한다.

What If I Need to Fetch Data at Request Time?

유저가 리퀘스트를 보내기 전에 페이지를 pre-render할 수 없다면 Static Generation은 좋은 방법이 아니다. 페이지의 데이터가 자주 업데이트되거나 매 리퀘스트마다 페이지의 컨텐츠다 달라지는 경우가 될 수 있을 것이다.

이러한 경우에는, Server-side Rendering이나 pre-rendering을 사용하지 않는 것이 좋다.

Fetching Data at Request Time

(실습과는 관계 없는 챕터)
빌드 타임이 아닌 리퀘스트 시점에 데이터를 fetch하고 싶다면 Server-side Rendering을 시도해보자.

서버사이드 렌더링을 하기 위해서는 getStaticProps 대신에 getServerSideProps를 export하면 된다.

Using getServerSideProps

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

getServerSideProps가 리퀘스트 시점에 불려지기 때문에 해당 파라미터인 (content)는 구체적인 파라미터를 포함해야 한다.

getServerSideProps는 리퀘스트 시점에 데이터가 fetch 되어야 하는 페이지를 pre-render 할 때만 사용해야 한다. Time to first byte(TTFB)가 getStaticProps보다 느리기 때문인데, 이는 서버가 매 리퀘스트마다 결과 화면을 컴퓨팅해야하고, 그 결과가 추가적인 명시가 없다면 CDN에 의해 캐시되지 않기 때문이다.

Client-side Rendering

데이터를 pre-render 할 필요가 없다면 클라이언트 사이드 렌더링을 사용할 수 있다.

  • 외부 데이터가 필요하지 않은 페이지 부분을 정적으로 생성(pre-render)한다.
  • 페이지가 로딩되면 JavaScript를 사용하여 클라이언트에서 외부 데이터를 가져오고 나머지 부분을 채운다.

이러한 접근 방식은 사용자 대쉬보드 페이지에 적합하다. 대쉬보드는 비공개이며 사용자 별로 렌더되는 페이지기 때문에 SEO와 관련이 없으며 미리 렌더링할 필요도 없다. 데이터는 자주 업데이트 되므로 요청시에 데이터를 가져와야 한다.

SWR

Next.js 팀에서는 SWR이라는 데이터를 가져오기 위한 React Hook을 만들었다. client side에서 fetching data를 한다면 추천하는 hook이다. SWR은 caching, revalidation, focus tracking, refetching on interval 등을 처리한다.

일단 이런게 있다는 것만 알아두자.

import useSWR from "swr"

function Profile() {
  const { data, error } = useSWR("/api/user", fetch)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}
profile
(~˘▾˘)~♫❝ 쉽게만 살아가면 재미없어 빙고 .ᐟ ❞•*¨*•.¸¸♪

2개의 댓글

comment-user-thumbnail
2020년 12월 21일

퍼가요~

1개의 답글