본문 바로가기

JavaScript/Next.js

Data Fetching 번역

페이지 문서에서는 Next.js가 두 가지 형태의 사전 렌더 방식, 동적 생성과 서버 사이드 렌더링이 있다는 것에 대해 설명하였다. 이 문서에서는 데이터 fetching의 각 방식에 대해 심도 있게 살펴볼 것이다. 아직 페이지 문서에 대해 읽어보지 않았다면 먼저 페이지 문서에 대해서 읽어보는 것을 추천한다.

 

사전 렌더링을 위해 데이터를 가져오는 데 사용할 수 있는 Next.js의 세 가지 독특한 방식에 대해 이야기할 것이다.

 

  • getStaticProps(정적 생성): 빌드 시간에 데이터를 가져오기
  • getStaticPaths(정적 생성): 데이터에 기초해서 특정 동적 경로를 사전 렌더하기
  • getServerSideProps(서버 사이드 렌더링): 매 요청마다 데이터를 가져오기

이에 더해서 우리는 클라이언트 측에서 어떻게 데이터를 가져올 것인지 짧게 살펴볼 것이다.

 

getStaticProps (정적 생성)

페이지로부터 getStaticProps라는 비동기 함수를 export 하면, Next.js는 getStaticProps로부터 리턴 받은 props를 사용하여 빌드 시간에 이 페이지를 사전 렌더 할 것이다.

 

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

context 파라미터는 아래의 키를 포함하고 있는 객체이다.

  • params는 동적 라우팅을 사용한 경로 파라미터를 포함하고 있다. 예를 들면 만약 페이지 이름이 [id].js이면 params는 {id:...}와 같을 것이다. 더 자세히 알아보기 위해선 Dynamic Routing documentation을 참조하라. getStaticPaths와 함께 사용해야만 한다. 추후에 설명할 것이다.
  • preview는 페이지의 프리뷰 모드에서 true이고 그렇지 않으면 undefined이다. Preview Mode documentation을 참조하라.
  • previewData는 setPreviewData에 의해서 정해진 프리뷰 데이터를 포함하고 있다. Preview Mode documentation을 참조하라.
  • locale은 가능한 경우 활성화된 locale을 포함한다.
  • locales는 가능한 경우 지원하는 모든 locales를 포함한다.
  • defaultLocale은 가능한 경우 설정된 기본 locale을 포함한다.

 

getStaticProps는 이러한 객체를 리턴해야 한다.

  • props - (required) 페이지 컴포넌트가 받을 props와 함께 필수적인 객체. 정렬 가능한 객체여야 한다(?)
  • revalidate - (optional) 해당 페이지가 몇 초 뒤에 재생성 할 것인지에 대한 선택적인 값. Incremental Static Regeneration을 참조하라.
  • notFound - (optional) 해당 페이지가 404를 리턴하게 할 것인지에 대한 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
  }
}

*getStaticPaths에서 리턴 받은 경로에 대해서만 사전 렌더 될 것이기 때문에 notFound는 fallback: false 모드에서는 필요하지 않다.

  • redirect - 내부나 외부의 자원으로 리다이렉트 하는 것을 허용할지에 대한 선택적인 값이다. { destination: string, permanent: boolean } 이 형식과 일치해야 한다. 몇몇 드문 경우 older HTTP에 대한 커스텀 상태 코드를 지정하는 것이 필요할 수도 있다. 이러한 경우에서는 permenent 항목 대신에 statusCode 항목을 사용할 수 있다. 하지만 둘 다 사용하는 것은 안된다. 아래는 어떻게 작동하는지에 대한 예시이다.
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
  }
}

*빌드 시간에 리다이렉트 되는 것은 현재 허용이 되지 않으며 리다이렉트가 빌드 시간에 알려지는 것(?)은 next.config.js에 추가가 되어야 한다.

노트: getStaticProps 안에서 API 경로를 호출하기 위해 fetch()를 사용해서는 안된다. 대신에 바로 API 라우트 내부에 사용된 로직을 바로 import하라. 이러한 방식으로 접근하기 위해서 코드를 살짝 리팩토링 해야 할 수도 있다.

 

Example

여기에는 getStaticProps를 사용하여 CMS로부터 블로그 포스트 리스트를 가져오는 예시가 있다.

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

언제 getStaticProps를 사용해야 할까?

이런 경우 getStaticProps를 사용해야 한다.

  • 해당 페이지를 렌더 하기 위해 필요한 데이터는 사용자가 요청하기 전 빌드 시간에 사용 가능(가져올 수 있다)하다.
  • 데이터는 headless CMS로부터 온다
  • 데이터는 공개적으로 캐시 가능하다(특정 유저별로 다른 것이 아닌)
  • 해당 페이지는 SEO를 위해 사전 렌더가 되어야 하고 매우 빨라야 한다. getStaticProps는 HTML과 JSON 파일을 생성할 수 있고 모두 성능을 위해서 CDN에 의해 캐시 될 수 있다.

TypeScript에서 getStaticProps 사용

TypeScript의 경우 next에서 GetStaticProps type을 사용할 수 있습니다.

import { GetStaticProps } from 'next'

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

props에 대한 유추된 type을 얻으려면 다음과 같이 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과 함께라면 정적 콘텐츠도 동적이 될 수 있기 때문에 동적 콘텐츠에만 의존할 필요는 없다. Incremental Static Regeneration은 이미 존재하는 페이지들을 트래픽이 들어올 때 백그라운드에서 리 렌더링 하여 업데이트할 수 있게 해 준다.

stale-while-revalidate에 영감을 받아 백그라운드 재생성은 트래픽이 항상 정적 스토리지로부터(?) 중단 없이 제공되도록 보장하고 새롭게 빌드된 페이지는 생성이 완료된 후에만 푸시된다.

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 10 seconds
    revalidate: 10, // In seconds
  }
}

// This function gets called at build time on server-side.
// It may be called again, on a serverless function, if
// the path has not been generated.
export async function getStaticPaths() {
  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: blocking } will server-render pages
  // on-demand if the path doesn't exist.
  return { paths, fallback: 'blocking' }
}

export default Blog

이제 블로그 포스트 리스트는 1초에 한 번씩 재검증된다. 새로운 블로그 포스트가 추가되는 경우 앱을 다시 빌드하거나 새롭게 배포할 필요 없이 거의 즉시 사용 가능할 것이다.

이것은 fallback:true와 함께 완벽히 동작한다. 왜냐하면 이제 항상 최신의 포스트들로 업데이트된 목록을 갖고 있을 수 있고 추가하거나 업데이트하는 게시물 수에 관계없이 주문형 블로그 게시물을 생성하는 블로그 페이지를 가질 수 있기 때문이다.

 

Reading files: Use process.cwd()

파일들은 getStaticProps에서 파일 시스템으로부터 바로 읽을 수 있다.

그렇게 하기 위해서는 파일의 전체 경로를 갖고 있어야 한다.

Next.js가 코드를 분리된 디렉터리에서 컴파일하기 때문에 페이지 디렉터리와 달라서 __dirname을 경로로 사용할 수 없다.

대신에 Next.js가 실행되는 곳의 디렉터리를 process.cwd()를 사용할 수 있다.

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

빌드 시에만 실행

getStaticProps가 빌드 시간에 동작하기 때문에 쿼리 파라미터나 정적인 HTML이 생성되면서의 HTTP 헤더와 같은 요청 시간 동안에만 가능한 데이터를 받지 않는다.

 

서버 측 코드 직접 작성

getStaticProps는 서버 측에서만 동작한다. 클라이언트 측에서는 절대로 동작하지 않는다. 브라우저를 위한 JS번들조차 포함하지 않을 것이다. 이것의 의미는 브라우저로 보내지 않는 직접적인 데이터베이스 쿼리와 같은 코드를 작성할 수 있다는 것이다. getStaticProps에서 API 라우트를 가져와서는 안되지만 대신에 서버 측의 코드는 getStaticProps에서 바로 작성 가능하다.

 

HTML과 JSON을 모두 정적으로 생성

getStaticProps를 갖고 있는 페이지가 빌드 시간에 사전 렌더 될 때 HTML 파일에 더해서 Next.js는 JSON파일을 생성하여 getStaticProps가 동작한 결과를 갖고 있는다.

이 JSON파일은 next/link나 next/router를 통한 클라이언트 측 라우팅에 사용될 것이다. getStaticProps를 사용하여 사전 렌더 된 페이지로 이동할 때 Next.js는 이 JSON파일(빌드 시간에 재 계산된) 가져오고 해당 페이지 컴포넌트의 props으로 사용할 것이다. 이것은 즉 export 된 JSON만 사용하기 때문에 클라이언트 측 페이지 전환이 getStaticProps를 호출하지 않을 것을 의미한다.

 

페이지에서만 허용됨

getStaticProps는 페이지에서만 export가 가능하다. 페이지가 아닌 파일에서 export는 불가능하다.

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

그리고 export async function getStaticProps() {}를 사용해야만 한다. 만일 getStaticProps를 페이지 컴포넌트의 속성으로 추가한다면 작동하지 않을 것이다.

 

개발 중인 모든 요청에 ​​대해 실행

개발 모드(next dev)에서 getStaticProps는 매 요청 시마다 호출될 것이다.

 

Preview Mode

어떤 경우에는 일시적으로 정적 생성을 통과하고 빌드 시간 대신 요청 시간에 렌더 하기를 원할 수도 있다. 예를 들면 headless CMS를 사용하고 있어서 초고가 퍼블리싱되기 전에 미리 보기를 원할 수 있다.

이러한 경우 Next.js는 프리뷰 모드라는 기능을 지원하고 있다. Preview Mode문서를 참조하라.

 

getServerSideProps (Server-side Rendering)

getServerSideProps라는 비동기 함수를 페이지에서 export 할 경우 Next.js는 이 페이지를 매 요청마다 getServerSideProps로부터 리턴 받은 데이터를 사용하여 사전 렌더 할 것이다.

context 파라미터는 아래의 키들을 포함하고 있는 객체이다.

  • params: 해당 페이지가 동적 경로를 사용한다면 params은 경로 파라미터를 포함하고 있다. 페이지의 이름이 [id].js이면 params는 { id: ... } 와 같이 보일 것이다. 더 알아보기를 원한다면 Dynamic Routing documentation을 참조하라.
  • req: HTTP IncomingMessage 객체
  • res: HTTP 응답 객체
  • query: 쿼리 스트링을 대표하는 객체
  • preview: priview가 true일 경우 해당 페이지는 프리뷰 모드로 들어가고 false이면 그렇지 않다. Preview Mode 문서를 참조하라.
  • previewData: 프리뷰 데이터는 setPreviewData에 의해서 설정된다. Preview Mode 문서를 참조하라.
  • resolvedUrl: 클라이언트 전환을 위해 _next / data 접두사를 제거하고 원래 쿼리 값을 포함하는 정규화된 요청 URL 버전이다. (구글 번역)
  • locale은 활성화된 장소를 포함한다(가능한 경우)
  • locales는 지원 가능한 모든 장소를 포함한다(가능한 경우)
  • defaultLocale은 설정된 기본 장소를 포함한다(가능한 경우)

getServerSideProps는 아래의 항목들을 포함한 객체를 반환한다:

  • props: 페이지 컴포넌트가 받을 props이 있는 필수 객체. 이 객체는 직렬 가능한 객체여야만 한다. 
  • notFound: 페이지가 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 : 내부나 외부의 자원으로 리다이렉트 하는 것을 허용할 것인지에 대한 선택 값. { destination: string, permanent: boolean } 이 형식과 일치해야 한다. 몇몇 드문 경우에는 구형 HTTP 클라이언트들에게 적합한 리다이렉트를 위해서 커스텀 상태 코드를 할당해주어야 할 수도 있다. 이러한 경우들에서는 permanent 프로퍼티 대신 statusCode 프로퍼티를 사용할 수 있는데 두 가지 프로퍼티를 모두 사용하는 것은 안된다. 아래의 예시가 어떻게 동작하는지를 보여준다.
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를 사용하기 위해서 상위 레벨 범위의 모듈들을 가져올 수 있다. getServerSideProps에서 사용되는 imports는 클라이언트 측을 위해 번들되지 않는다.

이 말의 의미는 getServerSideProps에 서버 측 코드를 직접 작성할 수 있다는 것이다. 여기에는 파일 시스템이나 데이터베이스에서 읽어오기 위한 것 역시 포함된다.

노트: getServerSideProps에서 API 경로에 있는 것을 호출하기 위해서 fetch()를 사용해서는 안된다. 대신에 API 경로 내에서 사용되는 로직을 직접 가져오라. 이렇게 접근하기 위해서 코드를 약간 리팩터링 해야 할 수도 있다.

외부 API로부터 가져오는 것은 문제없다!

 

Example

getServerSideProps를 사용하여 요청 시간에 데이터를 가져오고 사전 렌더 하는 예제이다. 이 예제는 Pages 문서에도 있다.

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

언제 getServerSideProps를 사용해야 합니까?

데이터를 반드시 요청 시간에 가져와야 하는 사전 렌더 페이지가 필요한 경우에만 getServerSideProps를 사용해야 한다. 서버가 매 요청의 결과를 계산해야 하고, 결과가 추가 설정 없이는 CDN에 의해 캐시 될 수 없기 때문에 Time to first byte(웹 서버의 반응성을 결정짓는 척도, 참조는 getStaticProps보다 느릴 것이다.

데이터를 사전 렌더 할 필요가 없다면 클라이언트 측에서 데이터를 가져오는 것을 고려해보아야 한다. 아래에 Fetching data on the client side에서 더 알아보라.

 

Technical details

서버 측에서만 실행

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

  • 이 페이지를 직접 요청했을 때, getServerSideProps 가 요청 시간에 동작하고 이 페이지는 반환받은 props로 사전 렌더 할 것이다.
  • 이 페이지를 클라이언트 사이드의 next/link 혹은 next/route를 사용한 전환을 통해서 요청되었다면 Next.js는 서버에다가 API를 요청할 것인데 이것은 getServerSideProps에서 돌아간다.

페이지에서만 허용됨

getServerSideProps는 페이지에서만 export 될 수 있다. 페이지가 아닌 파일에서는 export 할 수 없다.

또한 export async function getServerSideProps() {}를 사용해야만 한다. 만약 getServerSideProps를 페이지 컴포넌트의 프로퍼티로 추가한다면 작동하지 않을 것이다.

 

클라이언트 측에서 데이터 가져오기

만약 페이지에 자주 업데이트되는 데이터를 포함하고 있고 데이터를 사전 렌더 할 필요가 없다면 데이터를 클라이언트 측에서 가져올 수 있다. 이러한 예시로 유저에 특화된 데이터가 있다. 아래와 같이 동작한다:

  • 먼저 페이지가 데이터 없이 보인다. 페이지의 부분들은 정적 생성을 통해서 사전 렌더 될 수 있다. 채워지지 않은 데이터에 대해서는 로딩 상태로 보여줄 수 있다.
  • 그다음 클라이언트 측에서 데이터를 가져와서 준비가 되면 보여준다.

SWR

Next.js팀은 SWR이라는 데이터를 가져오기 위한 React hook을 만들었다. 만일 클라이언트 측에서 데이터를 가져온다면 SWR을 사용하는 것을 강력히 추천한다. SWR은 caching, revalidation, focus tracking, refetching on interval과 같은 기능들을 처리한다. 아래와 같이 사용할 수 있다.

 

import useSWR from 'swr'

const fetcher = (url) => fetch(url).then((res) => res.json())

function Profile() {
  const { data, error } = useSWR('/api/user', fetcher)

  if (error) return <div>failed to load</div>
  if (!data) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

 

'JavaScript > Next.js' 카테고리의 다른 글

CRA와 비교한 Next.js의 장점  (0) 2021.12.08
next-redux-wrapper가 필요한 이유  (0) 2021.12.01
Pages 번역  (0) 2021.11.19
내가 정리하는 Next.js  (0) 2021.11.06
Next.js Scripts  (0) 2021.11.03