본문 바로가기
Development

[22.04.21] Data Fetching in Next.js

by igy95 2024. 1. 9.

Next.js란?

The React Framework for Production.

 

공식 사이트를 타고 들어가면 가장 먼저 확인할 수 있는 문구다. 위 표현 그대로 Next.js는 대규모 서비스에 특화된 구조와 설계를 간편하게 만들 수 있도록 도와주는 리액트 프레임워크라고 할 수 있다. Next.js 내에서는 페이지를 렌더링 하는 다양한 방법을 지원하고 있기 때문에 각각의 장점을 두루 흡수할 수 있는데, 이 글에서는 다음과 같은 렌더링 기법의 특징과 해당 기법을 구현하기 위해 Next.js에서 제공하는 api들을 알아보려 한다.

 

  • CSR
  • SSR
  • SSG

CSR

CSR은 Client Side Rendering의 약자로, 데이터의 요청이 발생하면 클라이언트가 데이터를 가져와 html을 다시 그린다. 이는 리액트의 동작 방식과 동일하기 때문에 Next.js에서 따로 제공하는 api는 없으며 그저 useEffect()나 data fetching 라이브러리(react-query, SWR)를 통해 외부 데이터를 불러와 페이지를 그리면 된다.

 

이 기법은 주로 SEO의 적용이 필요없고 데이터의 업데이트가 빈번한 경우에 사용된다. 예를 들자면, 로그인 한 유저의 개인 프로필 페이지를 떠올려볼 수 있다. 프로필처럼 비공개로 관리되어 검색 사이트 노출을 위한 최적화와는 거리가 먼 특징의 페이지인 경우, 굳이 서버에서 매번 호출 로직을 수행하여 부하를 늘리기보다 CSR을 통해 부드러운 UX를 강조하는 것이 바람직하다.

SSR

SSR은 Server Side Rendering의 약자로, 데이터의 요청이 발생하면 서버가 데이터를 가져와 html을 미리 그려 응답으로 반환한다. Next.js는 SEO 적용이 필요하면서 요청에 따라 데이터가 바뀌어야 하는 페이지인 경우 SSR을 적용할 것을 권장한다.

 

공식문서에 따르면, SSR을 지원하는 api의 특징 중 하나는 자동 정적 최적화(Automatic Static Optimization)를 지원하지 않는다는 점이다. Next.js는 기본적으로 페이지 생성에 필요한 데이터가 없다고 판단하면, 자동으로 페이지를 정적으로 만들어둔다. 이렇게 최적화된 페이지는 서버의 계산이 따로 필요하지 않기 때문에 CDN 등을 통한 캐싱이 가능하고 결과적으로 사용자에게 빠른 로딩 경험을 제공한다. 하지만 SSR을 도입하게 되면, (별도의 캐싱 설정이 가능은 하지만) 요청마다 다른 페이지를 만들게 된다는 가능성을 내포하고 있기 때문에 꼭 필요할 때만 SSR api 도입을 고려해야 한다.

getInitialProps

function Page({ stars }) {
  return <div>Next stars: {stars}</div>;
}

Page.getInitialProps = async (ctx) => {
  const res = await fetch('https://api.github.com/repos/vercel/next.js');
  const json = await res.json();
  return { stars: json.stargazers_count };
};

export default Page;

Next.js 초창기에 서버 단에서 데이터를 주입하기 위해 제공되던 api다. 하지만 지금은 getServerSideProps, getStaticProps, getStaticPath에 비해 권장되지 않는다. 네이밍 자체도 다른 메서드에 비해 semantic하지 않을 뿐더러 해당 api를 사용할 때 몇가지 문제점이 존재하기 때문이다.

 

getInitialProps는 페이지 초기 로드 시에는 서버 단에서 호출하지만, next/link, next/router를 통해 페이지 간 이동이 발생하면 해당 api는 클라이언트 단에서 호출한다. 만약 서버 단에서 수행되어야 하는 로직이 getInitialProps에 존재한다면(e.g. DB 접근), 클라이언트에서 이것을 호출하는 시점에서는 문제가 발생한다. 또한 클라이언트에서도 호출이 되어야 하기 때문에 getInitialProps 코드 스니펫은 클라이언트 번들에 포함되고 이는 번들 사이즈의 증가를 야기한다.

getServerSideProps

function Page({ data }) {}

export async function getServerSideProps() {
  const res = await fetch(`https://.../data`);
  const data = await res.json();

  return {
    props: {
      data,
    },
  };
}

export default Page;

getServerSideProps는 앞선 getInitialProps와는 다르게, 페이지 진입이나 라우터를 통한 페이지 이동 상관없이 모두 서버 단에서 해당 api를 호출한다. 이러한 이점을 통해 사용자의 private한 데이터는 서버 단에서 처리하여 은닉할 수 있다는 장점을 가진다. 또한 클라이언트 번들에도 포함되지 않는다.

SSG

SSG는 Static Site Generation의 약자로 랜딩 페이지나 블로그 포스트처럼 한번 생성되고 데이터의 업데이트가 필요없는 페이지에 적용하기 가장 적합한 방법이다. 요청마다 페이지를 pre-render하여 반환하는 SSR과 다르게, SSG 페이지는 빌드 타임에 데이터를 가져와 한번만 html을 그려두고 매 요청마다 미리 그려둔 페이지를 반환한다.

getStaticProps

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

export async function getStaticProps() {
  const res = await fetch('https://.../posts');
  const posts = await res.json();

  return {
    props: {
      posts,
    },
  };
}

export default Blog;

전체적인 모양새와 반환값이 getServerSideProps와 유사하다. 하지만 위 api를 호출한 페이지는 빌드 타임에 정적으로 생성된다.

getStaticPath

// pages/posts/[id].js

export async function getStaticPaths() {
  return {
    paths: [
      {
        params: {
          id: '1',
        },
      },
      {
        params: {
          id: '2',
        },
      },
    ],
    fallback: true | false | 'blocking', // fallback을 필수로 반환해야 함
  };
}

export async function getStaticProps(context) {
  return {
    props: {
      post: {},
    },
  };
}

export default function Post({ post }) {}

동적 라우팅 페이지 중, 빌드 시에 정적으로 생성할 페이지의 경로를 설정해주는 api이다. 이 api를 사용하려면 getStaticProps를 같이 사용해야 한다. getStaticPaths의 반환값에는 paths와 fallback을 반환해야 하며 fallback 여부는 다음과 같이 나뉜다. (fallback 옵션은 빌드 타임에 생성해놓지 않은 경로로 요청이 들어올 경우를 대비한 설정이다.)

fallback: false

getStaticPaths에서 리턴하지 않는 경로는 모두 404로 연결한다. 생성할 경로 수가 적거나 페이지가 새로 추가되는 일이 드물다면 이 옵션을 써준다.

fallback: true

getStaticProps의 동작이 바뀐다.

 

  1. getStaticPaths가 반환한 경로의 페이지는 빌드 타임에 생성되어 사용자에게 바로 보여준다.
  2. 이외의 path로 요청이 들어올 경우 404 페이지를 반환하지 않고 페이지의 fallback을 먼저 보여준다. (로딩 UI, 스켈레톤 등)
  3. 그동안 백그라운드에서 Next.js가 요청받은 path에 대하여 getStaticProps를 호출해서 HTML, JSON을 정적으로 생성한다.
  4. 생성이 완료되면 브라우저는 생성된 JSON을 활용하여 해당 페이지를 렌더링한다. 동시에 Next.js는 이 페이지를 pre-render list에 저장하여 추후 동일한 요청이 왔을 때는 만들어둔 페이지를 바로 보여준다.

fallback 분기 처리는 next router의 isFallback 플래그로 가능하다.

// pages/posts/[id].js
import { useRouter } from 'next/router';

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

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  // render..
}

이 옵션은 대형 e 커머스와 같이 정적으로 생성할 페이지가 대량으로 있는 경우(e.g. 상품 페이지), 전체의 페이지를 빌드하기엔 빌드 시간이 오래 걸리니 일부만 미리 만들어두고 추후 요청에 따라 페이지를 그때그때 만들어주는 경우 유용하게 사용할 수 있다. 이러한 특징은 빌드 시간과 사용자의 응답 속도 모두 챙길 수 있다는 이점을 가지고 있다.

fallback: 'blocking'

fallback: true와 유사한 동작을 하지만, 이 옵션에서는 백그라운드에서 페이지를 생성하는 시간 동안 별도의 fallback 페이지를 보여주지 않고 SSR처럼 동작한다.

Server Data Fetching API를 사용할 때 주의사항

getServerSideProps, getStaticProps, getStaticPath는 서버 단에서만 호출되는 api이기 때문에 그 안에서 setTimeout, window.xxx, document.xxx와 같은 브라우저 api를 호출할 수 없다.

References