Next.js + Netlify-CMS (1)

Next.js를 학습하기 위해 사이트 프로젝트를 진행했습니다.

gitbook concept의 book 사이트로 활용하고자 만들었고, content관리를 위해 CMS를 사용했습니다.(netlify-cms)

Next.js Document가 매우매우 잘 되어있어서 좋았습니다.

https://nextjs.org/

이번 포스트는 간략하게 프로젝트를 설명하고,
Next.js document를 톺아보는 글을 작성해보겠습니다.
(아마 document를 몇개 포스트에 걸쳐 작성해야 될 것 같네요.)

1. Project Stack

https://seo-cms-nextjs.netlify.app/

  • nextjs
  • react
  • typescript
  • styled-components
  • netlify, netlify-cms
  • mdx

Folder structure

meta
├── authors.yml
├── categories.yml
└── tags.yml

public
├── _redirects
├── config.yml
└── images

src
├── components
│   ├── base
│   ├── common
│   ├── mdx
│   ├── posts
│   └── shared
├── interfaces
├── lib
│   ├── contexts
│   └── meta
├── pages
│   ├── api
│   │   └── users
│   ├── post
│   └── posts
│       └── categories
├── styles
│   └── lib
├── templates
└── utils

CNA

create-react-app 과 같이 developer tool을 nextjs에서 제공해주는 package가 있습니다.
(사실 next를 따로 설치하는 것이랑 별반 차이는 없습니다. 단지 적용할 stack에 대한 예제를 참고하고자 cna로 진행했습니다.)

https://nextjs.org/docs/api-reference/create-next-app
https://github.com/vercel/next.js/tree/canary/examples/with-typescript

typescript를 이용하기 위해 sample로 적용되어있는 with-typescript 프로젝트를 참조로 프로젝트를 생성합니다.

npx create-next-app --example with-typescript {project-name}

그 외 아주아주 많은 예제를 제공해주고 있으니 확인해보시면 도움이 될 것입니다.

https://github.com/vercel/next.js/tree/canary/examples

본격적으로 사용했던 코드를 참고하기 전에 Next.js에 대해서 알아보고자 합니다.

Next.js

The React Framework for Production
https://nextjs.org/

Next JS 는 React 를 이용하여 Client Side Rendering (CSR) 와 Server Side Rendering (SSR) 를 쉽게 혼합 하여 빠른 성능 을 낼 수 있게 도와주는 프레임워크입니다.

빌드 시점에 SSR 로 생성한 페이지로 First Meaningful Paint 를 앞당길 수 있으며, 이후 페이지를 이동할 때마다 Page Rendering 에 필요한 JSON_DATA를 가져와 필요한 부분만 다시 그림으로써 CSR 의 장점 또한 살릴 수 있습니다.

NextJS 로 Static Site 만들기

Next.js를 알아보기 위해 시작했으니 document를 한번 정리해본 것이 좋겠다고 생각이 들어 정리해보았습니다.


Next.js는 production에 필요한 기능(다음과 같은)
하이브리드 static 및 server rendering,
TypeScript 지원,
smart bundling,
route pre-fetching 등을 제공함으로써
최고의 개발자 경험을 제공합니다.

  • 서버 사이드에서 렌더링을 자동으로 제공합니다.
  • 자동으로 코드 스플릿을 해줍니다
  • 페이지 기반의 간단한 클라이언트 사이드 라우팅을 제공합니다.(next/link, next/router)
  • HMR(Hot Module Replacement)을 지원하는 Webpack 기반의 개발 환경을 제공합니다.
  • Express나 Node.js HTTP 서버를 구현할 수 있습니다.
  • 사용하고 있는 Babel, Webpack 설정을 원하는 대로 설정할 수 있습니다.(.babelrc, next.config.js)

Next.js Features

Next.js 특징 중 다음 2개에 대해서 알아봅시다.

  • Pages
  • Data fetching

Pages

Next.js에서 page/pages 폴더안에 .js .jsx .ts .tsx로부터 export된 React Component입니다.
https://nextjs.org/docs/basic-features/pages

예를들어 pages/about.js 파일을(jsx를 export) 만들었다면 그 파일은 /about 경로에서 접근될 때 사용될 것입니다.

다음과 같은 기능들을 제공합니다.

  • Dynamic Routes
    pages/posts/[id].js 와 같이 만들 수 있습니다.
    이 파일은 posts/1 posts/2 와 같은 곳에서 접근될 때 사용됩니다.
  • Pre-rendering
    기본적으로 Next.js는 모든 page를 pre-render합니다.
    이 말은 모든 page에 대해서 HTML파일을 미리 생성한다는 뜻입니다.
    실제로 요청이 오게 되면 js code가 동작되고 HTML과 완전히 interactive하게 만들고 요청에 대해 응답을 합니다.(이 과정을 hydration이라고 합니다.)

Pre-rendering 종류
1. Static Generation (Recommended): build 시에 생성된 HTML을 미리 가지고 있습니다. 그리고 요청이 오게 되면 그 파일을 재사용해서 응답합니다.
2. Server-side Rendering: 각 요청에 대해서 HTML을 생성합니다.

Next.js는 pre-rendering을 각 Page에 대해 개발자가 원하는 방향으로 선택하게 합니다.
hybrid App을 위해 대부분의 Page를 static generation을 사용하고 나머지 몇 page에 대해서 server-side rendering을 하는 것처럼 말이죠.

Next.js document에서는 성능상 이유로 Static Generation을 사용하길 권고하고 있습니다.
그 이유로는 정적으로 생성된 page는 성능 향상을 위한 추가 구성 없이 CDN에 의해서 cache될 수 있기 때문입니다.
(하지만 몇몇 경우에는 SSR이 유일한 옵션일 경우도 있습니다.)

무엇보다 이러한 기능과 함께 CSR 또한 함께 사용할 수 있습니다.
이말은 페이지 중 일부는 client side javascript에 의해 완전히 render될 수 있다는 것을 의미합니다.

Static Generation

page가 Static Generation을 사용하면 그 page는 build시에 pre-rendering HTML을 생성합니다.

Next.js는 data가 있든 없든(API로부터 fetch된 Data가 필요하든 안필요하든, DB로 부터 Data를 가져오든 안가져오든) 정적으로 페이지를 생성할 수 있습니다.

data가 없을 경우

function About() {
  return <div>About</div>
}

export default About

이런 경우에는 build 시에 single HTML파일을 생성합니다.


data가 있을 경우

몇몇 페이지는 pre-rendering을 위한 외부 data fetch가 필요합니다.

2가지 경우에 대해서 기술되어져 있습니다.

  1. page content가 외부 data에 의존되어 있을 경우:
    getStaticProps 사용
  2. page path가 외부 data에 의존되어 있을 경우
    getStaticPaths 사용
    (보통 getStaticProps도 같이 사용하게 됩니다.)

1. content가 외부 data에 의존되어 있을 경우

// TODO: pre-render하기 전에 외부 API Endpoint를 호출하는
//       `posts` 데이터가 필요함
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

export default Blog

pre-render를 위해 posts data를 fetch하기 위해서
같은 파일 내에 getStaticProps 함수를 사용합니다.
이 함수는 build시에 called하고 pre-render시에 fetch된 data를 props로 넘겨줄 것입니다.

function Blog({ posts }) {
  // Render posts...
}

// build 시에 호출 됩니다.
export async function getStaticProps() {
  // 외부 api 호출
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // posts는 props에 담겨 전달되어집니다.
  return {
    props: {
      posts,
    },
  }
}

export default Blog

참고로 typescript에 대해서 사용된 예제도 같이 보여드립니다.

import { GetStaticProps, InferGetStaticPropsType } from 'next';
...

//interface Props {
//  categories: CategoryContent[];
//  posts: PostContent[];
//}

const Index = ({ categories, posts }: InferGetStaticPropsType<typeof getStaticProps>) => (
  <Layout
    title="Posts"
    sideMenu={<SideMenu categories={categories} posts={posts} />}
  >
    <Title align="center">TIL</Title>
    <PostList posts={posts} />
  </Layout>
);

export const getStaticProps: GetStaticProps = async () => {
  const categories: CategoryContent[] = await listCategories();
  const posts: PostContent[] = await listPostContent();
  return { props: { categories, posts } };
};

2. path가 외부 data에 의존되어 있을 경우

dynamic route로 path를 만들었을 때를 생각해봅시다.

예를 들어 pages/posts/[id].js라는 파일을 만들었습니다.
id에 의해 single post를 보여주는 파일입니다.

만약 id 가 1 이라면 posts/1 에서 접근될 것입니다.
하지만 pre-render시에는 외부 data인 id를 얻을 수 없습니다.

그러면 어떻게 pre-render를 해야될까요?

database에 id가 1인 post가 단 하나만 있다고 생각해 봅시다.
그럼 pre-render는 posts/1 하나에 대해서 하면 됩니다.

나중에 id가 2인 post가 들어왔다고 하면, 그 때 pre-render를 1과 더불어 posts/2를 추가하면 될 것입니다.

그러면 이 id를, 다시말해 모든 path(여기서는 id)에 대해서 pre-render하면 되는 것이겠네요.

그 때 사용되는 함수가 getStaticPaths 입니다.

// 빌드 시에 call 됩니다.
export async function getStaticPaths() {
  // posts를 얻기 위해 api call 합니다.
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // pre-render시에 필요한 `id`를 얻습니다.
  const paths = posts.map((post) => `/posts/${post.id}`)

  // 이 path들을 build시에 pre-render하게 될 것입니다.
  // { fallback: false } 이 path이외의 route는 404로 가게됩니다.
  return { paths, fallback: false }
}

또한 getStaticProps를 통해서 id를 통해 post에 대한 내용을 fetch하는데 사용할 수 있습니다.

function Post({ post }) {
  // Render post...
}

export async function getStaticPaths() {
  // ...
}

// 빌드 시에 호출 됩니다.
export async function getStaticProps({ params }) {
  // params는 `id`를 가지고 있습니다.
  // 만약 route가 /posts/1 이라면 params.id는 1입니다.
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // Pass post data to the page via props
  return { props: { post } }
}

export default Post

언제 Static Generation을 사용해야 될까요?
Next.js는 가능한 모든 경우에 사용하라고 합니다.(data가 있든 없든)
왜냐하면 page를 단 한번만 build하고 CDN에 의해 제공될 수 있기 때문입니다.
그리고 그것은 server render보다 더 빠르게 만들어 줄 것입니다.

page를 static generation할지는 스스로에게 이 질문을 해보세요.
사용자 요청 이전에 이 페이지가 pre-render될 수 있는가?없는가?

Server-side Rendering

page가 SSR을 사용하면 그 page는 각 요청에 대해서 HTML을 생성합니다.

Server-side-Rendering을 위해 posts data를 fetch하기 위해서
같은 파일 내에 getServerSideProps 함수를 사용합니다.

위에 사용했던 예시를 생각해봅시다.

function Page({ data }) {
  // Render data...
}

// 요청이 올 때마다 call됩니다.
export async function getServerSideProps() {
  // 외부 API로 부터 fetch 합니다.
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  // props를 통해 data를 return 합니다.
  return { props: { data } }
}

export default Page

getStaticProps 와 사용법은 유사하게 사용합니다.
단지 차이점은 getStaticProps는 build시에 call되는 것이고 getServerSideProps는 요청이 올 때마다 call되는 것입니다.

Data Fetching

Static GenerationServer-side Rendering에 대해서 더 자세하게 알아보고자 합니다.

Data Fetching에는 pre-rendering을 위한 3가지 방법이 존재합니다.

  • getStaticProps : (Static Generation) build 시에 fetch data
  • getStaticPaths : (Static Generation) build 시에 fetch data를 기반으로 dynamic route를 지정
  • getServerSideProps : (Server-side Rendering) 각 요청 마다 fetch data

추가로 client side에서 data를 fetch 하는 방법 또한 간단하게 설명할 것입니다.

getStaticProps (Static Generation)

async 함수인 getStaticProps 함수를 export하면 Next.js는 getStaticProps에 의해 return된 props를 이용해 build 시에 pre-rendering 하게 될 것입니다.

export async function getStaticProps(context) {
  return {
    props: {}, // props로 page component에 전달될 것입니다.
  }
}

getStaticProps에서 context parameter는 다음과 같은 key를 가지는 object 입니다.

  • params :
    getStaticPaths함수와 함께 사용되어집니다.
    dynamic route에서 사용하는 값으로 route parameter를 가집니다.
    예를 들어 [id].js라는 page라면 params에는 { id: ... }와 같은 형태를 가집니다.
  • preview :
    preview mode에 있다면 true이고 그렇지 않으면 undefined 입니다.
    headless CMS에서 data를 fetch할 때 유용합니다.
    Next.js가 build time이 아닌 runtime 요청 시에 기존에 게시된 콘텐츠 대신 현재 CMS에서 draft된 콘텐츠를 render하길 원할 때 유용합니다.(https://nextjs.org/docs/advanced-features/preview-mode)
  • previewData :
    preview mode 시에 preview Data set을 가지고 있습니다.(cookie에 저장된다고 하네요)
  • locale : 현재 active locale을 포함합니다.
  • locales : 모든 supported locales를 포함합니다.
  • defaultLocale : 설정된 default locale을 포함합니다.

getStaticProps는 다음과 같은 object를 return 해야합니다.

  • props : required(object)
    page component가 받을 props object입니다. serializable object로 만들어야만 합니다.
  • revalidate : optional(number)
    page re-generate가 일어날 수 있는 시간(초 단위)
    예를들어 1초로 적용해놓으면 요청이 들어올 때 1초마다 page를 re-generation 합니다.
    새로운 post가 생성되더라도 재 build, 재 deploy 하는 것 없이 사용될 수 있습니다.
  • notFound : optional(boolean)
    404 page로 return 하도록 합니다.
    getStaticPaths에서 fallback: false 일 경우에는 필요하지 않습니다.
    pre-render되는 path들만 return 되기 때문입니다.
export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      notFound: true,
    }
  }

  return {
    props: {}, 
  }
}
  • redirect: optinal({ desination: string, permanent: boolean})
    내부나 외부 resource로 redirect 하게 합니다.
export async function getStaticProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }

  return {
    props: {},
  }
}

build 시에 redirecting은 현재 허용되지 않습니다.
build 시에 redirect를 허용하기 위해 next.config.js에 다음과 같이 추가 해야 됩니다.

 module.exports = {
   async redirects() {
     return [
       {
         source: '/about',
         destination: '/',
         permanent: true,
       },
     ]
   },
 }

getStaticProps에서 top-level scope module을 사용 할 수 있습니다.
getStaticProps에서 import된 module은 클라이언트 측에 번들로 제공되지 않습니다.
즉, getStaticProps에서 직접 서버 측 코드를 작성할 수 있습니다.
여기에는 파일 시스템이나 데이터베이스에서 읽는 것이 포함됩니다.

  • top-level scope module
import { something } from "<module>"; <-- Global / Top-level scope

getStaticProps에서 Application내에 API route를 호출하기 위해 fetch()를 사용해서는 안됩니다.
대신 API route 내에서 사용되는 로직(control)을 직접 가져와야 합니다.
이 접근 방식을 위해 코드를 약간 리팩토링해야 할 수도 있습니다.

외부 API에서 가져 오는 것은 괜찮습니다!
https://stackoverflow.com/questions/64040992/how-to-import-api-route-in-nextjs-for-getstaticprops

언제 getStaticProps를 사용해야 하나요?

  • 사용자 요청에 앞서서 build 시에 페이지를 pre-render에 이용할 수 있는 데이터가 있다면.
  • headless CMS로부터 오는 데이터가 있다면.
  • cached될 수 있는 데이터가 있다면(사용자 별로 말고).
  • SEO를 위해 pre-rendering 되어야만 하는 페이지라면.

Incremental Static Regeneration

증분 정적 재생성

트래픽이 유입될 때 백그라운드에서 페이지를 다시 렌더링하여 기존 페이지를 업데이트한다. 트래픽은 항상 정적 스토리지에서 중단 없이 제공되며, 새로 작성된 페이지는 생성 완료 후에만 푸시된다. 이는 기존 SSR과 달리 지연시간이 급증하지 않으며, 페이지가 오프라인으로 전환되지 않는다는 이점을 가진다.

https://reactions-demo.vercel.app/
https://github.com/chibicode/reactions/issues/1

getStaticProps를 사용할 때 페이지는 정적으로 생성되지만 콘텐츠는 동적 일 수 있습니다.
그러나 Next.js에서는 동적 콘텐츠를 위해 재 빌드하거나 재 배포할 필요는 없습니다.
Incremental Static Regeneration을 사용하면 트래픽이 들어올 때 백그라운드에서 다시 렌더링하여 기존 페이지를 업데이트 할 수 있습니다.

SWR(stale-while-revalidate)에서 영감받은 기술입니다.

stale-while-revaliate
cache-control 확장 디렉티브를 통해 cache 처리하는 방식 중 하나.

Cache-control: stale-while-revalidate=<seconds>


https://developers.google.com/web/tools/workbox/modules/workbox-strategies#stale-while-revalidate

요청 할 때에
cache가 있다면 cache,
cache가 없다면 network fallback

getStaticProps의 revalidate return을 통해 re-generation할 수 있게 합니다.

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// 이 함수는 server-side에서 build시에 에서 호출될 것입니다.
// 만약 revalidation이 허용되어있고 새로운 요청이 들어온다면,
// serverless function에 의해 재 호출 될 것입니다.
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js는 
    // - 요청이 들어 올때 최대 1초에 한번 씩 
    // - 해당 page를 re-generate를 시도할 것입니다.
    revalidate: 1, // In seconds
  }
}

export default Blog

이것은 fallback: true 일 경우에만 완벽하게 동작합니다.
왜 그런지는 getStaticPaths 설명 시에 확인해보도록 합시다.

https://nextjs.org/docs/basic-features/data-fetching#fallback-pages

process.cwd()을 사용하세요

Next.js는 코드를 directory별로 컴파일하므로
반환할 경로가 pages 디렉터리와 다르기 때문에 __dirname을 사용할 수 없습니다.

대신 prcess.cwd()를 사용할 수 있습니다.

import fs from 'fs'
import path from 'path'

function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

// server-side에서 빌드 시에 호출 됩니다.
// client-side에서는 호출되지 않습니다.
// database 쿼리도 direct로 사용할 수 있습니다.
export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts') 
  const filenames = fs.readdirSync(postsDirectory)

  const posts = filenames.map((filename) => {
    const filePath = path.join(postsDirectory, filename)
    const fileContents = fs.readFileSync(filePath, 'utf8')

    // 일반적으로 post파일들을 
    // parse, transform하여 전달합니다.
    return {
      filename,
      content: fileContents,
    }
  })
  
  return {
    props: {
      posts,
    },
  }
}

export default Blog

getStaticProps Technical details

  • build 시에만 동작
    getStaticProps는 build시에만 동작하기 때문에 HTML Header나 query param과 같은 요청 시에만 이용가능한 데이터는 받을 수 없다는 것을 기억하세요.
  • server-side 코드를 바로 작성하세요.
    Application내에 정의된 API Route를 호출하지 말고, server-side 로직을 바로 사용하세요.(외부 API가 아닌 내부)
  • HTML과 JSON 둘 다 정적으로 생성됩니다.
    getStaticProps가 빌드 시에 pre-rendering 할 때 HTML파일과 더불어 getStaticProps 결과를 가지는 JSON파일도 같이 생성합니다.
    이 JSON 파일은 client-side routing 에 사용될 것입니다.
    (next/link next/router)
    다시 말하자면, client-side page는 getStaticProps를 호출하지 않고, JSON 파일만 사용할 것입니다.
  • page에서만 허용됩니다.
    non-page에서는 사용할 수 없습니다.
    이러한 제한의 이유 중 하나는 React가 render되기 전에 필요한 데이터를 가질 필요가 있기 때문입니다.
    (주의 : 이름이 동일한 getStaticProps property가 있다면 동작하지 않습니다.)
  • 개발 시에는 모든 요청에 대해 동작합니다.
    next dev로 실행 시에는 getStaticProps는 매 요청마다 호출 될 것입니다.
  • Preview Mode
    때떄로 Static Generation을 bypass해야될 때가 있을 것입니다.
    build 일 때가 아닌 요청 시에 render가 되길 원할 때가 있을 것입니다.
    예를 들어, headless CMS를 사용 시에
    작성은 되었지만 아직 publish가 되지 않은 draft에 대해서 보여지길 원할 수 있습니다.
    그럴 때 Preview Mode를 사용할 수 있습니다.

getStaticPaths (Static Generation)

dynamic route를 이용한다면, build 시에 HTML을 생성하기 위해 path list를 정의해야할 필요가 있습니다.
이런 경우에 getStaticPaths를 사용합니다.

getStaticPaths를 사용하면 Next.js는 getStaticPaths에 정의된 모든 path에 대해 정적으로 pre-rendering을 할 것입니다.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } 
    ],
    fallback: true or false 
  };
}

return에 해당되는 key 정보는 다음과 같습니다.

paths => (required)

pre-render되는 path를 결정합니다.

가령 pages/posts/[id].js로 dynamic route를 만들었다고 가정해봅시다.

그리고 다음과 같이 paths를 정의했다고 생각해봅시다.

return {
  paths: [
    { params: { id: '1' } },
    { params: { id: '2' } }
  ],
  fallback: ...
}

Next.js는 posts/1 posts/2pages/posts/[id].js를 사용하여 build 시에 정적으로 생성 할 것입니다.

params는 dynamic route에서 page pathname으로 사용된 parameter와 match되어야만 합니다.

page pathname을 정의하는 몇가지 케이스가 있습니다.

  • fixed route
    pages/posts/[postId]/[commentId]
    paramspostIdcommentId를 가져야만 합니다.
  • catch-all route
    pages/[...slug] <- array destructuring
    catch-all route를 사용한다면 paramsslug array를 반드시 가져야만 합니다.
    예를 들어 ['foo', 'bar']/foo/bar 를 정적으로 생성 할 것입니다.
  • optinal catch-all route
    pages/[[...slug]]
    catch-all route를 선택적으로 사용한다면, root-most route를 render하기 위해 null, [], undefined, false를 제공하면 됩니다.
    만약 pages/[[...slug]]로 만들고 slug: false를 제공한다면 Next.js는 / page를 생성 할 것입니다.
    (optinal 차이점은 parameter가 없을 때에 root-most route로 매치된다는 점입니다.)
    https://nextjs.org/docs/api-routes/dynamic-api-routes#optional-catch-all-api-routes
// GET `/api/post` (empty object)
{ } 
// `GET /api/post/a` (single-element array)
{ "slug": ["a"] } 
// `GET /api/post/a/b` (multi-element array)
{ "slug": ["a", "b"] } 

fallback => (required)

boolean type인 fallback을 return 해야만 합니다.

  • fallback: false
    getStaticPaths에 path로 정의되지 않은 것은 404 page로 응답할 것입니다.
    pre-render하는 path list가 많지 않다면(build시에 모두 정적 생성되기 때문입니다) 사용할 수 있습니다.
    새로운 page가 추가되었을 때는 build를 새로해야 404 page로 가지 않을 것입니다.
function Post({ post }) {
  // Render post...
}

// build 시에 호출 됩니다.
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()
  
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // - paths에 대해서만 정적 생성 합니다.
  // - { fallback: false } 그 외 다른 route에 
  //   대해서는 404 page 를 return 할 것입니다.
  return { paths, fallback: false }
}

export async function getStaticProps({ params }) {
  // 위에서 설정한 대로 params는 id를 가집니다
  // route가 /posts/1 이라면 params.id는 1 입니다.
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  // props를 통해서 Post component로 전달될 것입니다.
  return { props: { post } }
}

export default Post
  • fallback: true
    1. build 시에 생성 되지 않았던 path들은 404 page가 되지는 않습니다. 대신 Next.js는 이러한 path들에 대해서 첫 요청이 들어오면 "대체" 버전(fallback 버전)을 제공합니다.
    2. background에서는 Next.js가 요청된 path에 대해 정적으로 HTML과 JSON을 생성할 것이고 getStaticProps에 넘겨줄 것입니다.
    3. 사용자 관점에서는 fallback page에서 완전한 page로 swap될 것입니다.
    4. 동시에 Next.js는 이 path를 pre-render page 리스트에 추가할 것입니다.
    5. 그리고 후속 요청은 정적 생성된 page(build 시 pre-rendering 페이지)와 동일하게 제공합니다.

    next export 사용시에는 제공되지 않습니다.

Fallback page(fallback: true)

page에 fallback 버전을 넣어줍시다.

  • page props는 empty일 것입니다.
  • next/router를 사용해 fallback 인지 체크하면 됩니다.
    router.isFallback === true
// pages/posts/[id].js
import { useRouter } from 'next/router'

function Post({ post }) {
  const router = useRouter()

  // 만약 아직 page가 생성되지 않았다면, 아래와 같이
  // Loading...이라는 글자를 보여줍니다.
  // (getStaticProps() 동작이 끝나기 전까지)
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  return {
    // 정적 생성 : `/posts/1` and `/posts/2`
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // `/posts/3` 와 같은 
    fallback: true,
  }
}

export async function getStaticProps({ params }) {
  const res = await fetch(`https://.../posts/${params.id}`)
  const post = await res.json()

  return {
    props: { post },
    // 요청이 들어온다면
    // 매 1초당 post를 재 생성 할것입니다.
    revalidate: 1,
  }
}

export default Post
  • fallback: true는 언제 유용한가요?
    매우 많은 수의 정적 페이지들(e-commerce site와 같은? 방대한 데이터에 의존하는)이 있는 app 에서 유용합니다.

fallback: 'blocking'

getStaticPaths에서 정의하지 않은 새로운 path에 대해서는 HTML이 생성될 때까지 기다릴 것입니다.(SSR과 동일)
그리고 추후 요청을 위해 cache 될 것입니다.(path 당 한번 발생)

  1. build 시에 생성 되지 않았던 path들은 404 page가 되지는 않습니다. 대신 Next.js는 이러한 path들에 대해서 첫 요청이 들어오면 SSR을 할 것입니다.
  2. 사용자 관점에서는 'page를 요청 중'에서 완전한 page로 전환될 것입니다. 여기에는 loading/fallback state 순간이 없습니다.
  3. 동시에 Next.js는 이 path를 pre-render page 리스트에 추가할 것입니다.
  4. 그리고 후속 요청은 정적 생성된 page(build 시 pre-rendering 페이지)와 동일하게 제공합니다.

next export 사용시에는 제공되지 않습니다.

fallback: 'blocking'은 이미 생성된 page에 대해서 update되지 않습니다.
이미 생성된 page를 업데이트 하기 위해서는 Incremental Static Regeneration와 결합해서 사용해야 합니다.

getStaticPaths는 언제 사용해야 하나요?

  • dynamic route를 사용한 정적 pre-rendering page가 있다면 사용해야만 합니다.

getStaticPaths Technical details

  • getStaticProps와 함께 사용하세요
    dynamic route param을 가진 page에서.
    (getServerSideProps와 함께 사용할 수 없습니다.)
  • server-side에서 build 시에만 동작합니다.
  • page에서만 허용됩니다.
    non-page에서는 export 될 수 없습니다.
    (주의 : 이름이 동일한 getStaticPaths property가 있다면 동작하지 않습니다.)
  • 개발 시에는 모든 요청에 대해 동작합니다.
    next dev로 실행 시에는 getStaticPaths는 매 요청마다 호출 될 것입니다.

getServerSideProps (Server-side Rendering)

Next.js는 getServerSidProps에 의해 return된 data를 사용해 매 요청 시 page를 pre-render할 것입니다.

export async function getServerSideProps(context) {
  return {
    props: {}, 
  }
}

context parmas는 다음을 가지는 object입니다.

  • params : dynamic route를 사용한다면 params는 route parameter를 가집니다.
    [id].js 일 경우 params: { id : ... } 형태가 될 것입니다.
  • req : HTTP IncomingMessage Object
  • res : HTTP Response Object
  • query : query string
  • preview : preview Mode일 경우 true 그렇지 않으면 false 입니다.
  • previewData : setPreivewData에 의한 preview Data Set입니다.
  • resolveUrl : _next/data 접두사를 제거하고 원래 쿼리 값을 포함하는 요청 URL의 정규화 된 버전입니다.
  • locale : 현재 active locale을 포함합니다.
  • locales : 모든 supported locales를 포함합니다.
  • defaultLocale : 설정된 default locale을 포함합니다.

return object입니다.

  • props : required(object)
    page component가 받을 props object입니다. serializable object로 만들어야만 합니다.
  • notFound : optional(boolean)
    404 page로 return 하도록 합니다.
  • redirect: optinal({ desination: string, permanent: boolean})
    내부나 외부 resource로 redirect 하게 합니다.
export async function getServerSideProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

  if (!data) {
    return {
      redirect: {
        destination: '/',
        permanent: false,
      },
    }
  }

  return {
    props: {}, // will be passed to the page component as props
  }
}

getServerSideProps에서도 getStaticProps와 같이 top-level scope module을 사용 하더라도
import된 module은 클라이언트 측에 번들로 제공되지 않습니다.

getServerSideProps에서도 getStaticProps와 마찬가지로 Application내에 API route를 호출하기 위해 fetch()를 사용해서는 안됩니다.

getServerSideProps는 언제 사용하나요?
요청을 할 때 fetch되어야만 하는 전체 데이터를 page에 pre-render해야할 필요가 있을 경우에만 사용합니다.

TTFB (Time to First byte)는 서버가 모든 요청에 대해 결과를 계산해야하고 추가 설정 없이는 CDN에 cache 될 수 없기 때문에 getStaticProps보다 느립니다.

만약 data를 pre-render할 필요가 없다면 client-side에서 data를 fetch하는 것도 고려해야 합니다.

getServerSideProps Technical details

  • server-side에서만 동작합니다.
    -요청이 왔을 때 동작합니다.
    -next/link, next/router와 같이 client-side에서 요청이 왔을 때는 Next.js가 getServerSideProps를 실행하는 server에 API 요청을 보냅니다. 이것은 Next.js가 자동으로 다루기 때문에 getServerSideProps가 정의되어있는 한 추가 작업을 수행 할 필요가 없습니다.
  • page에서만 허용됩니다.
    non-page에서는 export 될 수 없습니다.
    (주의 : 이름이 동일한 getStaticPaths property가 있다면 동작하지 않습니다.)
  • 개발 시에는 모든 요청에 대해 동작합니다.
    next dev로 실행 시에는 getStaticPaths는 매 요청마다 호출 될 것입니다.

Fetching Data on Client Side

만약 data를 pre-render할 필요가 없다면 client-side에서 data를 fetch하는 것도 고려해야 합니다.

  • 우선, data가 없는 page를 즉시 보여줍니다. (like skeleton, loading 등)
  • 그리고 client-side에서 data가 fetch되면 그것을 보여줍니다.

이 접근법은 user dashboard page에 유용합니다.
(private하고 user별로 구성된 page이고 SEO와 상관없기 때문입니다.)
요청 시 마다 data를 fetch하기 때문에 update에 유연합니다.(SSR)

SWR

client-side에서 data를 fetch한다면 이 hook을 강력히 추천합니다.

  • handle caching
  • revalidation
  • focus tracking: page focus를 인지
  • refetching on interval
  • more...
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>
}

(useSWR)


Next.js 에서 사용되는 주요 특징들 중 Pages, Data Fetching 부분을 살펴보았습니다.

확실히 Document를 다시 한번 훑어보는게 처음 시작하면서 이해한 것보다 더 많이 이해가 되는 것 같습니다.

사실 이해가 느려서 작성하는데에는 더 오래 걸리는 것 같네요.

다음 post에서는 css, image, static file 등 asset과 관련된 특징을 살펴보도록 하고, 어느 정도 document를 훑어본 후에
제가 프로젝트에서 어떻게 사용했는지 다시 확인하면서 refactoring 하는 것도 괜찮을 것 같단 생각이 듭니다.
(사실 아직 구현안된게 훨씬 많아서...)

좋은 웹페이지 즐겨찾기