Data Fetching

broccoli·2021년 4월 13일
0

next문서번역

목록 보기
3/5
post-thumbnail

Pages 문서에서 Next.js는 2종류의 pre-rendering이 있다고 설명했다: Static GenerationServer-side Rendering. 이제 각 경우의 data 가져오는 전략의 대해 좀 더 깊이 알아보자.

pre-rendering시 data를 가지고 오기 위해 사용하는 Next.js의 3가지 유니크한 함수들에 대해 이야기하자.

  • getStaticProps(Static Generation): 빌드시에 data를 가지고 온다.
  • getStaticPaths(Static Generation): pre-render 페이지의 동적경로를 지정한다.
  • getStaticProps(Static Generation): 매 요청시에 data를 가지고 온다

추가로 client-side에서 어떻게 data를 가지고 오는지도 간략히 이야기 하자.

getStaticProps(Static Generation)

만약 해당 page에 getStaticProps라 불리는 async 함수를 export한다면 Next.js는 이 페이지는 빌드시에 pre-render할 거고 props 는 getStatciProps의 의해 리턴된다.

export async function getStaticProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

context 파라미터는 다음의 속성들을 포함한 object 이다.

  • params 는 dynamic routes를 사용하는 페이지들의 라우트 파라미터를 포함한다. 예를 들어 만약 페이지의 이름이 [id].js라면 params{id: ...} 같이 될 것이다. 좀 더 상세한 정보는 Dynamic Routing 문서를 살펴봐라. 이 params 속성은 getStaticPaths 와 함께 사용해야 한다.
  • previewtrue이다. 해당 페이지가 preview mode인 경우가 아니라면 undefined이다. 자세항 사항은 Preview Mode문서를 참조해라.
  • previewDatasetPreviewData에 의해 세팅된 data를 포함한다. Preview Mode 문서를 참조해라.
  • locale은 active locale을 포함한다.(enabled 되어있다면)
  • locales는 지원되는 모든 locales를 포함한다.(enabled 되어있다면)
  • defaultLocale은 설정한 디폴트 locale을 포함한다.(enabled 되어있다면)

getStaticProps는 아래 사항들을 포함된 objects를 리턴해야한다.

  • props - 이건 해당 페이지 컴포넌트에 의해 전달받아야하는 required object이다. serializable한 객채여야한다.
  • revalidate - 이건 몇초후에 해당 페이지를 재생성 할지의 대한 optional 수치값이다.
  • notFound - 이건 404상태 및 해당 페이지를 리턴하도록 하는 optional한 boolean값이다. 아래에 예시가 있다.
export async function getStaticProps(context) {
  const res = await fetch(`https://.../data`)
  const data = await res.json()

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

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

fallback: false모드는 notFound가 필요없다. 왜냐하면 getStaticPaths에서 반환된 경로들만 pre-render되기 때문이다.

  • redirect - 이 optional redirect 값은 내부나 외부 자원으로 redirecting 할 수 있게 해준다. 이 값은 받드시 {destination: string, permanet: boolean} 형태여야 한다. 드문 경우로 이전 HTTP Client가 적절하게 redirect 되도록 커스텀 상태코드를 할당해야할수도 있는데 이경우는 statusCode 속성을 permanent 속성 대신 사용할 수 있지만 둘다 함께는 사용 못한다. 아래는 동작예시이다.
export async function getStaticProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()

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

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

현재 Redirecting을 빌드시에 하는 것은 허용되지 않는다. 만약 redirects가 빌드시에 알려질 필요가 있다면 반드시 next.config.js 에 그 redirects를 포함해라.

top-level scope에 getStaticProps를 사용한 모듈을 import할 수 있고 이 imports 모듈은 client-side에 번들되지 않을 것이다.
이 말은 getStaticPropsserver-side code를 직접 작성할 수 있다는 의미이다. 여기에는 파일시스템이나 데이터베이스에서 읽는 것까지 포함한다.

getStaticProps에서는 API route를 호출하는 fetch()를 사용하면 안된다. 대신 API route 내부에서 사용되는 로직을 직접 import하는 것은 된다. 이런 접근 방식을 위해서는 코드를 약간 리팩토링 할 필요가 있을 수 있다.
외부 API로 부터 Fetching 하는건 괜찮다.

Simple Example

여기 CMS(conent management system)로부터 블로그 포스트리스트를 fetch하기 위해getStaticProps를 사용한 예가 있다. 이 예는 또한 Pages 문서에도 있다.

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>{post.title}</li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
  // Call an external API endpoint to get posts.
  // You can use any data fetching library
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts,
    },
  }
}

export default Blog

When should I use getStaticProps?

getStaticProps는 이 때 사용해라:

  • render되는 페이지가 필요한 데이터가 사용자 요청 전 빌드시에 활용가능한 data 일때
  • data가 headless CMS에서 오는 data일때
  • data가 사용자개인화가 필요하지 않은 공공성의 캐시된 data일때
  • 해당 페이지가 SEO를 위해 반드시 pre-render 된 페이지 여야하고 매우 빨리 로드되어야 할 때, -- getStaticProps는 HTML과 JSON파일을 생성하고 둘 다 CDN에 의해 캐시되서 성능적으로 매우 좋다.

TypeScript: Use GetStaticProps

타입스크립트의 경우 next에서 GetStaticProps 유형을 사용할 수 있다.

import { GetStaticProps } from 'next'

export const getStaticProps: GetStaticProps = async (context) => {
  // ...
}

만약 도출한 특정 타입을 원한다면 InferGetStaticPropsType<typeof getStaticProps>를 아래와 같이 사용해라.

import { InferGetStaticPropsType } from 'next'

type Post = {
  author: string
  content: string
}

export const getStaticProps = async () => {
  const res = await fetch('https://.../posts')
  const posts: Post[] = await res.json()

  return {
    props: {
      posts,
    },
  }
}

function Blog({ posts }: InferGetStaticPropsType<typeof getStaticProps>) {
  // will resolve posts to type Post[]
}

export default Blog

Incremental Static Regeneration

getStaticProps을 사용한다면 더이상 dynamic content에 종속될 필요가 없다. static content도 dynamic content가 될 수있다. Incremental Static REgeneration을 사용하면 트래픽이 들어올때 백그라운드에서 다시 렌더랑해서 기존 페이지를 업데이트 할 수 있게 해준다.

stale-while-revalidate 에 의해 영감받은 백그라운드 재생성은 트래픽이 항상 정적 저장소에서 중단없이 제공됨을 보장하고 새로 빌드된 페이지는 생성이 완료됬을 경우에만 푸시된다.

이전 gesStaticProps 예를 생각해보자. 이제 재생성이 활성화 되었다.

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

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// revalidation is enabled and a new request comes in
export async function getStaticProps() {
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  return {
    props: {
      posts,
    },
    // Next.js will attempt to re-generate the page:
    // - When a request comes in
    // - At most once every second
    revalidate: 1, // In seconds
  }
}

export default Blog

이제 블로그 포스트리스트는 1초마다 재생성될 것이다. 만약 새로운 블로그 포스트가 추가되면 그것은 앱을 재빌드하거나 재배포없이 거의 곧바로 활용가능해진다.

그리고 이건 fallback:true 옵션과 완벽히 동작한다. 왜냐하면 이제 항상 새로운 최신 포스트 리스트를 가질 수 있고 얼마나 많은 포스트가 추가되고 수정되더라도 요구에 따라 생성된 블로그 포스트를 가질 수 있다.

Statci content at scale

전통적인 SSR과는 다르게 Incremental Static Regeneration은 정적특징의 장점을 보장해준다.

  • 지연시간이 급중하지 않는다. 페이지는 지속적으로 빠르게 제공된다.
  • 페이지는 절대 오프라인되지 않는다. 만약 백그라운드 페이지 재생성이 실패한다면 예전 페이지는 변경되지 않고 유지된다.
  • db나 백엔드에 로드 부하가 작다. 왜냐면 페이즈들은 동시에 최대 한번 재계산되기 때문이다.

Reading Files: Use process.cwd()

파일들은 getStaticProps에서 직접 파일시스템을 통해 읽혀 질 수 있다.
그렇게 하기 위해서 반드시 파일의 full path를 가지고 있어야한다.

Next.js는 코드를 몇개의 디렉토리 속으로 나누어서 컴파일한다. 따라서 __dirname를 사용하면 실제 페이지 디렉토리 경로와 다른 경로를 리턴하기 때문에 사용할 수 없다.

대신 process.cwd()를 사용할 수 있다. 이것은 Next.js 실행이 일어나는 디렉토리 정보를 준다.

import { promises as fs } from 'fs'
import path from 'path'

// posts will be populated at build time by getStaticProps()
function Blog({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li>
          <h3>{post.filename}</h3>
          <p>{post.content}</p>
        </li>
      ))}
    </ul>
  )
}

// This function gets called at build time on server-side.
// It won't be called on client-side, so you can even do
// direct database queries. See the "Technical details" section.
export async function getStaticProps() {
  const postsDirectory = path.join(process.cwd(), 'posts')
  const filenames = await fs.readdir(postsDirectory)

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

    // Generally you would parse/transform the contents
    // For example you can transform markdown to HTML here

    return {
      filename,
      content: fileContents,
    }
  })
  // By returning { props: { posts } }, the Blog component
  // will receive `posts` as a prop at build time
  return {
    props: {
      posts: await Promise.all(posts),
    },
  }
}

export default Blog

Technical details

Only runs at build time

getStaticProps는 빌드시에만 동작하기 때문에 정적 HTML을 생성하는 동안 쿼리파라미터나 HTTP Header 같은, 요청이 일어나는 동안만 사용가능한 data를 받을 수 없다.

Write server-side code directly

getStaticProps는 서버사이드에서만 동작한다. 결코 클라이언트 사이드에서 동작하지 않을 것이다. 또한 브라우저에서 확인 가능한 js bundle에 조차 포함되지 않을 것이다. 이것이 의미하는 것은 굳이 브라우저에 정보를 요청할 필요없이 direct database queries를 직접 사용해서 정보를 가져올 수 있다는 것이다. getStaticProps로부터 API route를 fetch 하면 안된다. 대신 getStaticProps에서 server-side 코드를 직접 작성할 수는 있다.

ℹ️ getStaticProps로부터 API route를 fetch 하면 안된다.
이말이 헷갈릴수 있는데 아래 인용문을 보면 이해하는 데 도움이 될것이다.

getStaticProps (Static Generation) fetches data at build time and only processed there. That means there is no mix between frontend and backend. If you need data in getStaticProps, you should not cause an extra network call to /api to get the data, instead you should use a function with that logic or directly in getStaticProps.
If you need data in your frontend, you can fetch data from your rest/graphql/whatever/api or pass it via getServerSideProps.

이 예를 통해서 Next.js가 클라이언트 사이트 번들로 부터 무엇을 제거하는지 확인 할 수 있다.

// This app shows what Next.js bundles for the client-side with its new SSG
// support. This editor supports TypeScript syntax.
import Cookies from 'cookies';
import Mysql from 'mysql';
import Link from 'next/link';
import SQL from 'sql-template-strings';
import Layout from '../components/Layout';

const pool = Mysql.createPool(process.env.DATABASE_URL);

export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

export async function getServerSideProps({ req, res }) {
  const userId = new Cookies(req, res).get('user_id');
  const projects = await new Promise((resolve, reject) =>
    pool.query(
      SQL`SELECT id, name FROM projects WHERE user_id = ${userId};`,
      (err, results) => (err ? reject(err) : resolve(results))
    )
  );
  return { props: { projects } };
}
// This is the code that is bundled for the client-side:

import Link from 'next/link';
import Layout from '../components/Layout';
export var __N_SSP = true;
export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

Statically Generates both HTML and JSON

getStaticProps를 가지고 있는 페이지가 빌드시 pre-render될 때, Next.js는 HTML파일뿐만아니라 getStaticProps의 실행결과를 포함하는 JSON파일도 생성한다.

이 JSON파일은 next/linknext/router를 통해 client-side 라우팅에서 사용될 것이다. getStaticProps로 pre-render된 페이지로 이동할 때 Next.js는 이 JSON파일(빌드시 계산된)을 가지고 오고 그것을 해당 페이지 컴포넌트의 props로 사용한다. 이것은 client-side transition은 getStaticProps를 호출하는 것이아니라 단지 exported JSON 을 사용한다는 것을 의미한다.

Incremental Static Generation 을 사용할때 getStaticProps는 client-side 탐색의 필요에 의한 JSON을 생성하기위해 대역외에서 실행될것이다. 이것이 동일페이지에 대한 multiple 요청에 형태로 볼 수 있지만 이것은 의도된 것이고 end사용자의 퍼포먼스에 영향을 주지 않는다.

Only allowed in a page

getStaticPropspage에서만 exported 된다. 다른 곳에서는 사용할 수없다.

이러한 제한의 한가지 이유는 리액트는 페이지가 render 되기 전에 필요한 data 모두를 가지고 있어야 하기 때문이다.

또한 반드시 export async function getStaticProps() {} 로 사용해야한다. getStaticProps를 페이지 컴포넌트의 속성으로 쓴다면 동작하지 않을 것이다.

Runs on every request in development

개발환경(next dev)에서 getStaticProps는 매 요청시에 호출된다.

Preview Mode

몇가지 경우에서 임시적인 bypass Static Generation을 원할 지도 모른다. 그리고 요청시에 페이지가 render 되길 원할지도 모르다. 한가지 예로써 만약 headless CMS를 사용하고 있고 그것들이 배포되기전에 프리뷰 초안을 보길 원할 수 있다.

이런 경우 Next.js는 Preview Mode라 불리는 특성을 지원해준다. 자세한 사항은 Preview Mode문서를 참조해라.

getStaticPaths(Static Generation)

만약 dynamic route를 가지고 있는 페이지가 있고 그 페이지가 getStaticProps를 사용한다면 그 페이지는 빌드시에 HTML pre-render될때 어떤 경로의 리스트의 페이지들인지 정의할 필요가 있다.

만약 dynamic routes를 사용하는 어떤 페이지에서 getStaticPaths라 불리는 async 함수를 export 한다면, Next.js는 getStaticPaths에 의해 정의된 모든 paths들을 정적으로 pre-render할 것이다.

export async function getStaticPaths() {
  return {
    paths: [
      { params: { ... } } // See the "paths" section below
    ],
    fallback: true or false // See the "fallback" section below
  };
}

The paths key(required)

paths 키는 pre-render되야하는 경로들을 정의한 것이다. pages/posts/[id].js라는 이름의 dynamic routes를 사용하는 페이지가 있다고 가정해보자. getStaticPaths를 해당 페이지에서 export하고 이것은 paths를 다음과 같이 리턴한다.

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

그럼 Next.js는 posts/1, posts/2 를 빌드시에 정적으로 생성할 것이다. 그리고 이건 pages/posts/[id].js의 페이지 컴포넌트로서 사용된다.

params 의 값은 반드시 해당 페이지의 이름으로 사용될 파라미터와 매핑되어야 한다.

  • 만약 페이지 명이 pages/posts/[postId]/[commentId]라면 params는 반드시 postIdcommentId를 포함해야한다.
  • 만약 페이지명이 catch-all routes를 사용한다 가정하자. 예를들어 pages/[...slug]라고 한다면 params는 반드시 slug를 배열로서 포함해야한다. 만약 이 배열이 ['foo', 'bar]라면 Next.js는 foo/bar라는 페이지를 정적으로 생성할 것이다.
  • 만약 페이지가 optional catch-all routes(null, [], undefined, false)를 사용한다 가정하자. 예를들어 pages/[[...slug]]를 위해 slug:false를 제공한다면 Next.js는 / page를 생성할 것이다.

The fallbackkey (required)

getStaticPaths에 의해 리턴되는 객체는 반드시 boolean 속성의 fallback 키를 포함해야한다.

fallback: false

만약 fallbackfalse이고 getStaticPaths에 의해 전달받지 못한 pathes들은 모두 404 페이지로 호출된다. 만약 적은양의 pre-render 경로들을 가지고 있다면 이렇게 설정 할 수 있다. 이 페이지들은 빌드시 모두 정적으로 생성된다. 그리고 만약 새 페이지가 자주 추가되지 않는다면 이방식은 유용하다. 만약 데이터 소스에 항목을 더 추가하고 새 페이지를 렌더링해야하는 경우엔 빌드를 다시 실행해야한다.

만약 pages/posts/[id].js라 불리는 페이지를 예를 들어보자. getStaticPaths에 의해 CMS로 부터 블로그 포스트리스트는 리턴된다. 그리고 각 페이지들은 getStaticProps에 의해 CMS로부터 각 포스트 데이터가 리턴된다. 아래 예는 Pages 문서에도 있다.

// pages/posts/[id].js

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

// This function gets called at build time
export async function getStaticPaths() {
  // Call an external API endpoint to get posts
  const res = await fetch('https://.../posts')
  const posts = await res.json()

  // Get the paths we want to pre-render based on posts
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))

  // We'll pre-render only these paths at build time.
  // { fallback: false } means other routes should 404.
  return { paths, fallback: false }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 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

fallback: true

만약 fallbacktrue이면 getStaticProps의 행동이 변한다.

  • getStaticPaths로 부터 리턴된 경로들은 getStaticProps의해 빌드시에 HTML로 render된다.
  • 빌드시에 생성되지 않은 경로들은 404 페이지로 로드되지 않는다. 대신 Next.js는 해당 페이지의 첫번째요청시에 "fallback"버전을 제공한다. (자세한 내용은 "Fallback Pages"를 봐라)
  • getStaticProps를 실행하는 것을 포함해서 백그라운드로 Next.js는 요청된 경로의 HTML과 JSON을 정적으로 생성할 것이다.
  • 이과정이 모두 완료되면 브라우저는 생성된 경로를 위해 JSON을 받는다. 이것은 제공된 props 와 해당페이지의 자동적은 render에 사용될 것이다. 사용자 관점에서 이 페이지는 fallback 페이지에서 Full page 로 전환될 것이다.
  • 그와 동시에 Next.js는 pre-render된 페이지의 리스트에 이 경로를 추가한다. 동일한 경로에 연속적인 요청은 빌드시 pre-render된 다른페이지들 처럼 생성된 페이지를 제공할 것이다.

fallback: truenext next 사용시 지원되지 않는다.

Fallback Pages: 로딩페이지 같은 역활 가능

"fallback" 버전의 페이지에서는

  • 페이지의 props는 비워질 것이다.
  • 라우터사용시 fallback이 렌더되면 router.isFallbacktrue 값이 되는데 이로인해 fallback이 렌더링 되었다는 것을 확인할 수 있다.
// pages/posts/[id].js
import { useRouter } from 'next/router'

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

  // If the page is not yet generated, this will be displayed
  // initially until getStaticProps() finishes running
  if (router.isFallback) {
    return <div>Loading...</div>
  }

  // Render post...
}

// This function gets called at build time
export async function getStaticPaths() {
  return {
    // Only `/posts/1` and `/posts/2` are generated at build time
    paths: [{ params: { id: '1' } }, { params: { id: '2' } }],
    // Enable statically generating additional pages
    // For example: `/posts/3`
    fallback: true,
  }
}

// This also gets called at build time
export async function getStaticProps({ params }) {
  // params contains the post `id`.
  // If the route is like /posts/1, then params.id is 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 },
    // Re-generate the post at most once per second
    // if a request comes in
    revalidate: 1,
  }
}

export default Post

When is fallback: true useful?

fallback: true는 app이 매우 많은 양의 정적페이지(매우 큰 e-commerce site)를 가지고 있을때 유용하다. 모든 상품페이지를 다 pre-render 하고 싶지만 그렇게 되면 빌드시간이 엄청엄청 오래걸린다.

대신 특정페이지의 작은 집합들을 정적으로 생성하고 fallback: true로 나머지를 사용하면 된다. 어떤 사람들은 아직 생성되지 않은 페이지들을 요청할 수 있는데 그때 그 유저는 로딩창을 볼 수 있을 것이다. 짧은 시간 이후 getStaticProps는 완료되고 요청된 데이터와 함께 페이지는 렌더될것이다. 그 이후 동일한 페이지를 요청한 모든 사람은 정적으로 pre-render 페이지를 얻을 것이다.

이런 방식은 Static Generation의 장점을 그대로 가져감과 동시에 빠른 빌드 그리고 사용자가 항상 빠른 경험을 하도록 보장해준다.

fallback: true는 생성된 페이지들을 업데이트하지 않을 것이다. 페이지가 업데이트가 되길 원한다면 Incremental Static Regeneration을 참조해라.

fallback: 'blocking'

만약 fallbackblocking이라면 getStaticPaths에 의해 리턴되지 않은 새로운 페이지들은 HTML이 생성될때 까지 기다릴 것이다. 이것은 SSR과 동일하다. 그리고 이후 동일한 요청에 대해서는 캐시가 된다.

getStaticProps는 다음과 같이 행동할 것이다.

  • getStaticPaths로 부터 리턴된 경로들은 getStaticProps에 의해 빌드시 HTML로 render된다.
  • 빌드시에 생성되지 않은 경로들은 404페이지로 가지 않는다. 대신 Next.js가 SSR처럼 최초요청에 한해서 행동할 것이고 HTML이 리턴된다
  • 동작이 완료되면 브라우저는 생성된 경로의 대한 HTML을 받고, 사용자 관점에서는 브라우저가 해당 페이지를 요청하고 요청된 페이지가 로드되는 것처럼 보인다. 무슨말이냐면 loading/fallback 상태가 없고 그냥 뽁하고 요청된 페이지가 로드된다는 것이다.
  • 동시에 Next.js는 이 경로를 pre-render 페이지들에 추가하고 이 동일 경로의 대한 추가 요청들은 일반적으로 pre-render된 다른 페이지처럼 제공한다

fallback: blocking은 디폴트로 생성된 페이지를 업데이트 하지 않는다. 생성된 페이지를 업데이트 하기 위해선 Incremental Static Regeneration 과 fallback: 'blocking'을 같이 사용해야 한다.

fallbakc: 'blocking'next export사용시 지원되지 않는다.

When should I use getStaticPaths?

동적 라우트를 사용해서 페이지를 정적으로 pre-render 한다면 getStaticPaths는 반드시 사용해야한다.

TypeScript: Use GetStaticPaths

타입스크립트에서는 next에서 GetStaticPaths를 사용할 수 있다.

import { GetStaticPaths } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  // ...
}

Technical details

Use together with getStaticProps

getStaticProps를 동적 라우트 파라미터와 함께 사용한다면 getStaticPaths는 무조건 써야한다. getStaticPathsgetServerSideprops와 절대 사용 같이 할 수 없다.

Only runs at build time on server-side

getStaticPaths는 서버사이드에서 빌드시에만 실행된다.

Only allowed in a page

getStaticPathspage에서만 expored 될 수 있다. page가 아닌곳에서는 export할 수 없다.

Runs on every request in development

개발환겨에서는 (next dev), getStaticPaths는 매 요청시에 호출된다.

getServerSideProps(Server-side Rendering)

만약 getServerSideProps라 불리는 async함수를 export한다면 Next.js는 getServerSideProps 에 의해 리턴된 data를 사용하면서 이 페이지를 매 요청시마다 pre-render할 것이다.

export async function getServerSideProps(context) {
  return {
    props: {}, // will be passed to the page component as props
  }
}

context 파라미터는 다음 속성들을 포함하고 있는 객체이다.

  • params: 만약 동적 라우트를 사용하는 페이지라면 params는 라우트 파라미터들을 포함한다. 만약 페이지 명이 [id].js라면 params{id:...}와 같은 형태가 될 것이다. 좀 더 자세하게 알기 원한다면 Dynamic Routing 문서를 찾아봐라
  • req: HTTP IncommingMessage object
  • res: HTTP response object
  • query: An object representing the query string
  • preview: previewtrue라면 이 페이지는 preview 모드 인것이고 false라면 아닌것이다. 자세한 사항은 Preview Mode 문서를 찾아봐라
  • previewData: setPreviewData에 의해 세팅된 preview data이다. 자세한 사항은 Preview Mode 문서를 찾아봐라.
  • resolvedUrl: client transition을 위해 _next/data 접두어가 생략된 요청 url의 정규화 버전이다. 원본 쿼리값도 포함되어있다.
  • locale: active locale을 포함한다.(if enabled)
  • locales: 모든 지원 locales를 포함한다.(if enabled)
  • defaultLocale: 디폴트로 설정된 locale을 포함한다.(if enabled)

getServerSideProps가 리턴하는 객체는 아래항목들이 포함한다.

  • props - 페이지 컴포넌트에 의해 리턴되는 props는 필수객체이다. 이것은 serializable object여야한다.
  • notFound - 페이지가 404 상태와 404페이지로 리턴되도록 허용하는 옵셔널한 boolean 값이다. 아래 동작예가 있다.
export async function getServerSideProps(context) {
  const res = await fetch(`https://...`)
  const data = await res.json()

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

  return {
    props: {}, // will be passed to the page component as props
  }
}
  • redirect: 옵셔널한 redirect 값으로 내외부 자원으로 redirecting 을 해주는 값이다. 반드시 {destination: string, permanent: boolean} 형태여야한다. 드문경우로 만약 이전 HTTP Clients에게 적절한 redict를 위해 custom 상태코드를 할당해야하는 경우가 있을 수 있다. 이 때에는 statusCodepermanent 대신 사용할 수 있다 단지 둘 모두를 같이 쓸 수는 없다. 아래는 동작 예시이다.
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는 top-level scope에 import 해서 모듈로 쓸 수 있다. 이것은 client-side에서 번들링 되지 않는다.
이 말은 getServerSideProps 안에 서버사이드 코드를 직접작성할 수 있다는 의미이고 파일시스템과 db를 읽는 것도 포함된다.

getServerSideProps에는 API route를 호출하는 fetch()를 사용해선 안된다. 대신 API route 내부에서 사용되는 로직을 직접 import해라. 이런 방식을 위해서는 코드가 리팩토링 될 여지가 있을 수있다.
외부 API로부터 Fetching하는건 괜찮다.

Simple example

여기에 사용자 요청시에 data를 가져오고 pre-render 하는 getServerSideProps 사용 예가 있다.

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

// This gets called on every request
export async function getServerSideProps() {
  // Fetch data from external API
  const res = await fetch(`https://.../data`)
  const data = await res.json()

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

export default Page

When should I use getServerSideProps?

getServerSideProps는 pre-render 되는 페이지의 필요한 데이터가 요청시에 가져와야하는 경우에만 사용해라. getStaticProps보다는 좀 느릴것이다. 왜냐면 서버가 반드시 모든 요청에 대한 결과를 계산해야만 하기 때문이다. 그리고 그 결과는 특정 설정 없이는 CDN에 의해 캐시될수 없다.

만약 데이터가 pre-render 될 필요 없다면, client-side에서 데이터를 가져오는 것을 고려해봐라.

TypeScript: Use GetServerSideProps

타입스크립트를 사용할 때는 next에서 GetServerSideProps를 사용할 수있다.

import { GetServerSideProps } from 'next'

export const getServerSideProps: GetServerSideProps = async (context) => {
  // ...
}

만약 props가 도출된 타입이 되길 원한다면 InferGetServerSidePropsType<typeofgetServerSideProps>를 사용할 수 있다.

import { InferGetServerSidePropsType } from 'next'

type Data = { ... }

export const getServerSideProps = async () => {
  const res = await fetch('https://.../data')
  const data: Data = await res.json()

  return {
    props: {
      data,
    },
  }
}

function Page({ data }: InferGetServerSidePropsType<typeof getServerSideProps>) {
  // will resolve posts to type Data
}

export default Page

Technical details

Only runs on server-side

getServerSideProps는 서버에서만 동작하고 결고 브라우저에서 동작하지 않는다. 만약 페이지가 getServerSideProps를 사용한다면

  • getServerSideProps는 요청시에만 실행된다 그리고 이페이지는 리턴된 props 와 함께 pre-render 된다.
  • 만약 이 페이지가 next/linknext/router를 통해 client-side transitions 에서 요청된다면 Next.js는 API 요청을 서버에 보낸다. 그리고 getServerSideProps를 실행한다. 그리고 getServerSideProps의 실행결과로 만들어진 JSON을 리턴한다. 그리고 그 JSON은 그 페이지에 render에 사용될 것이다. 이 모든 과정은 Next.js 에 의해 자동적으로 핸들될 것이다. 그러니 getServerSideProps만 잘 정의하면 된다.

아래 예를 통해 Next.js가 client-side 번들시에 어떻게 서버사이드부분 코드를 제거하는지 확인할 수 있다.

// This app shows what Next.js bundles for the client-side with its new SSG
// support. This editor supports TypeScript syntax.
import Cookies from 'cookies';
import Mysql from 'mysql';
import Link from 'next/link';
import SQL from 'sql-template-strings';
import Layout from '../components/Layout';

const pool = Mysql.createPool(process.env.DATABASE_URL);

export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

export async function getServerSideProps({ req, res }) {
  const userId = new Cookies(req, res).get('user_id');
  const projects = await new Promise((resolve, reject) =>
    pool.query(
      SQL`SELECT id, name FROM projects WHERE user_id = ${userId};`,
      (err, results) => (err ? reject(err) : resolve(results))
    )
  );
  return { props: { projects } };
}
// This is the code that is bundled for the client-side:

import Link from 'next/link';
import Layout from '../components/Layout';
export var __N_SSP = true;
export default function ({ projects }) {
  return (
    <Layout>
      <h1>Projects</h1>
      <ul>
        {projects.map((project) => (
          <li key={project.id}>
            <Link href="/projects/[id]" as={`/projects/${project.id}`}>
              <a>{project.name}</a>
            </Link>
          </li>
        ))}
      </ul>
    </Layout>
  );
}

Only allowed in a page

getServerSidePropspage안에서만 export될 수있다. 페이지가 아니면 export할 수 없다.

또한 export async function getServerSideProps() {} 으로 사용해야 하며 페이지 컴포넌트의 속성값으로 getServerSideProps를 추가한다면 동작하지 않을 것이다.

Fetching data on the client side

만약 페이지가 자주 업데이트 되는 data를 포함한다면 그 데이터는 pre-render할 필요가 없다. 클라이언트 사이드에서 데이터를 가져올 수 있다. 이것의 예로 사용자 개인화 데이터를 들 수 있다.

여기 동작예가 있다.

  • 먼저 데이터 없이 해당 페이지를 보여줘라. 해당 페이즈의 부분들은 Static Generation이 사용될 수 있다. 그리고 아직 가져오지 못한 데이터는 loading state를 보여라.
  • 그리고 data를 클라이언트 사이드에서 가져오고 준비가 되면 보여줘라.

이런 접근은 사용자 대시보드페이지에 적합하다. 왜냐하면 대시보드는 개인적이고 특별한 페이지이기 때문이다. SEO에도 연관없고 해당 페이지는 pre-render 될 필요도 없다. 데이터는 빈번히 업데이트 된다는 것은 요청시의 데이터가 가져올 필요가 있다는 의미이다.

SWR

Next.js팀은 SWR이라 불리는 data fetching 리액트 훅을 개발했다. 클라이언트사이드에서 데이터를 가져올때 이것이 굉장히 강추임. 이것은 캐싱, revalidation, focus tracking, 일정시간에 다시 가져오는 기능 등이 있다. 아래 예가 있다.

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

자세한 사항은 SWR문서를 확인해라.

profile
🌃브로콜리한 개발자🌟

0개의 댓글