Next 13 Data Fetch - 1

신대현·2023년 8월 24일
3

NextJS

목록 보기
2/2
post-thumbnail

시작하기 전

앱 라우트 next 13이 안정화된 후 사용해보지 못해, 언젠가는 사용하지 않을까 하는 생각에 직접 사용해보면서 이를 정리해보려고 합니다.
🙈 직접 해보고 제가 이해하고 있는선에서 정리 하는거라 잘못된 부분이 있다면 피드백 해주시면 적극 반영하겠습니다

예제는
여기서👈👈 확인 가능합니다

1. 사용방법

next 공식 홈페이지 Data Fetching 하는 방법은 총 4가지라고 설명하고 있습니다. 4가지 전부 사용해보도록 하겠습니다!

  • fetch API 를 사용하여 서버에서 데이터 가져오기
  • 라이브러리를 사용하여 서버에서 데이터 가져오기
  • Route Handler를 사용하여 데이터 가져오기
  • 라이브러리를 사용하여 클라이어트에서 데이터 가져오기

2. fetch API 를 사용하여 서버에서 데이터 가져오기

App Router에서는 이제 getStaticProps, getServerSideProps를 더 이상 사용하지 않습니다.
Page Router와 비교하여 이를 어떻게 사용해야 하는지 살펴보겠습니다.

Caching Data

AS-IS getStaticProps

interface Props {
	posts:Posts[]
}
const Page = (props:Props) => {
  const { posts } = props
  return (
    <main>
      <ul className="flex flex-col gap-30">
        {posts?.map((item) => {
          return (
            <li key={item.id}>
              <Link href={`/ssr/${item.id}`}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
};

export const getStaticProps:GetStaticProps = async () => {
    const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
  return {
  	props:{
    	posts
    }
  }
}

TO-BE

기본적으로 next는 fetch API init option에 force-cache 값을 넣어주면, fetch의 반환 값을 서버의 데이터 캐시에 자동으로 캐시하도록 되어 있습니다. fetch의 cache 기본값은 force-cache 입니다.

// 'force-cache' 가 기본값이므로 생략 가능합니다.
fetch('https://...', { cache: 'force-cache' })

App Router 의 getStaticProps는 아래와 같습니다.

❗❗ 타입스크립트가 포함된 서버 컴포넌트에서 async/await을 사용하려면 TypeScript 5.1.3 이상 및 @types/react 18.2.8 이상 사용해야 합니다.

// page.tsx
export default async function Page() {
    const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[]

  return (
    <main>
      <ul className="flex flex-col gap-30">
        {posts?.map((item) => {
          return (
            <li key={item.id}>
              <Link href={`/ssg/${item.id}`}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
}

정상적으로 Caching 이 되었다면 런타임 환경 log에 cache 가 HIT 가 된걸 확인 가능합니다.

Revalidating Data

AS-IS ISR

기존에는 getStaticProps에서 반환할 때 revalidate를 추가하여 재검증 시간을 지정했습니다.

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[]

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

TO-BE

APP Router에서는 Fetch API에 revalidate 옵션 값을 추가하여 재검증 시간을 지정할 수 있도록 변경되었습니다.
또는 Route Segment Config 을 사용하도록 되어있습니다

// page.tsx
export default async function Page() {
    const posts = 
          (await fetch(`https://jsonplaceholder.typicode.com/posts`, 
          { next: { revalidate: 3600 } }).then((res) => res.json())) as Posts[]

  return (
    <main>
      <ul className="flex flex-col gap-30">
        {posts?.map((item) => {
          return (
            <li key={item.id}>
              <Link href={`/ssg/${item.id}`}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
}

generateStaticParams
APP Router에 새로 추가된 기능인 generateStaticParams입니다. getStaticPaths를 대체하는 기능입니다.

AS-IS getStaticPaths

export const getStaticPaths: GetStaticPaths = async () => {
  const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
  const paths = data.map((res) => ({ id: String(res.id) }));
  return { paths, fallback: "blocking" };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
    const posts = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
  ).then(res=>res.json());

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

TO-BE
약간 다른 부분이 있다면 getStaticPaths에서 blocking | true 옵션을 주어 페이지가 존재하는지 확인할 수 있었지만, APP Router에서는 dynamicParams를 통해 제어가 가능해졌습니다. default 값은 true이므로 변경할 필요는 없지만, 사용하지 않으려는 경우 false로 설정할 수 있습니다.

//page.tsx
export async function generateStaticParams(): Promise<{ id: string }[]> {
  const data = await fetch(
    `https://jsonplaceholder.typicode.com/posts`,
  ).then((res)=>res.json());
  
  return data!.map((res) => ({ id: String(res.id) }));
}

export const dynamicParams = true

export default async function Page({ params }: Props) {
  const data = await fetch(
    `https://jsonplaceholder.typicode.com/posts/${params.id}`,
  ).then((res)=>res.json());

  return (
    <div className="flex flex-col">
      <div className="">
        <h1 className="truncate text-2xl font-medium capitalize text-gray-200">
          {data?.title}
        </h1>
        <p className="line-clamp-3 font-medium text-gray-500">{data?.body}</p>
      </div>
    </div>
  );
}

Opting out of Data Caching
마지막으로 fetch API 의 no-store 입니다 getServerSideProps를 대체하는 기능입니다.

AS-IS getServerSideProps

export const getServerSideProps: GetServerSideProps = async () => {
   const posts = (await fetch(`https://jsonplaceholder.typicode.com/posts`).then((res) => res.json())) as Posts[];
  return {
  	props:{
    	posts
    }
  }
};

TO-BE

const Page = async () => {
  const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`, {
    cache: 'no-cache',
  }).then((res) => res.json())) as Posts[];

  return (
    <main>
      <ul className="flex flex-col gap-30">
        {data?.map((item) => {
          return (
            <li key={item.id}>
              <Link href={`/ssr/${item.id}`}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
};

3. 라이브러리를 사용하여 서버에서 데이터 가져오기

외부 라이브러리(ex: axios)를 사용하여 데이터를 가져오는 경우는 아직 개발 중인 것 같습니다만 공식 홈페이지의 예제를 따라 사용해 보겠습니다.

다른점이 있다면 fetch API 같은 경우 옵션을 주입하여 렌더링 방식을 선택 했다면 axios 같은 라이브러리 같은경우 Route Segment Config 를 사용해야 합니다.

import axios from 'axios';
import { cache } from 'react';

export const revalidate = 3600

const getPosts = cache(async () => {
  return await axios.get(`https://jsonplaceholder.typicode.com/posts`);
});

const Page = async () => {
  const data = await getPosts()
  return (
    <main>
      <ul className="flex flex-col gap-30">
        {data?.map((item) => {
          return (
            <li key={item.id}>
              <Link href={`/ssr/${item.id}`}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
              </Link>
            </li>
          );
        })}
      </ul>
    </main>
  );
};

4. Route Handler를 사용하여 데이터 가져오기

Route Handler는 서버에서 실행되고 데이터를 클라이언트에 반환합니다. 이 기능은 API 토큰이나 민감한 정보를 클라이언트에 노출하고 싶지 않을 때 유용합니다.

// /app/api/posts/route.ts
import { NextResponse } from 'next/server';

export async function GET() {
  const data = await fetch('https://jsonplaceholder.typicode.com/posts', {
    headers: {
      'Content-Type': 'application/json',
      'API-Key': '클라이언트에서는_못보지롱',
      'Token':'클라이언트에_노출이되면_안되는_토큰'
    },
  }).then((res) => res.json());
  return NextResponse.json({ data });
}

// page.tsx 
'use client';
import { useEffect } from 'react';

const Page = () => {
  const [data,setData] =useState(null)
  
  const handleFetch = async () => {
	const res = await fetch('/api/posts').then((res)=>res.json());
    setData(res)
  }
  
  useEffect(() => {
	 handleFetch()
  }, []);
  
  return (
    <main>
      <ul className="flex flex-col gap-30">
        {data?.map((item) => {
          return (
            <li key={item.id}>
                <h1>{item.title}</h1>
                <p>{item.body}</p>
            </li>
          );
        })}
      </ul>
    </main>
  );
};

5. 라이브러리를 사용하여 클라이어트에서 데이터 가져오기

마지막으로, React-QuerySWR와 같은 라이브러리를 사용하는 경우입니다. 저는 react-query를 사용하겠습니다.
Next.js에서 react-query를 사용할 때 가장 중요한 pre-render를 어떻게 하는지 알아보겠습니다.

AS-IS

// _app.tsx
const App = ({Component, pageProps}: AppPropsWithLayout) => {
  const [queryClient] = useState(() => new QueryClient())

  return (
        <QueryClientProvider client={queryClient}>
          <Hydrate state={pageProps.dehydratedState}>
             <Component {...pageProps} />
          </Hydrate>
        </QueryClientProvider>
  )
}

export default App

// home.tsx
const getPosts = () => {
    const data = (await fetch(`https://jsonplaceholder.typicode.com/posts`, {
    cache: 'no-cache',
  }).then((res) => res.json())) as Posts[];
  
  return data
}

const HomePage:NextPage = () =>{
  const query = useQuery(['posts], () => getPosts())
  return (
  	...
  )
}

export async function getStaticProps() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery(['posts'], getPosts)

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    },
  }
}

TO-BE
App Router 에서 다른점이 있다면 _app.tsx 에서 pageProps 를 받아 하이드레이션 작업을 해주었는데 App Router 에서는 각 page.tsx 에서 하이드레이션을 해주어야 합니다.

간단하게 아래 처럼 바꼇다고 보면 될꺼 같습니다.

// page router
getStaticProps => _app.tsx => Hydrate => react-query
// app router
page.tsx => Hydrate => react-query

app router 에서는 서버 컴포넌트가 default 이기 때문에 react-query를 사용 하기 위해선 몇가지 작업을 해주어야 합니다.

// src/app/registry.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { PropsWithChildren, useState } from 'react';

const QueryClientRegistry = ({ children }: PropsWithChildren) => {
  const [queryClient] = useState(() => new QueryClient());

  return (
    <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
  );
};

export default QueryClientRegistry;
// src/utils/getQueryClient.ts
import { QueryClient } from "@tanstack/query-core";
import { cache } from "react";

const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;
// src/utils/hydrate.client.tsx
"use client";

import { Hydrate as RQHydrate, HydrateProps } from "@tanstack/react-query";

function Hydrate(props: HydrateProps) {
  return <RQHydrate {...props} />;
}

export default Hydrate;

작성하셨다면 page.tsx로 가서 사용해보겠습니다.

주의 해야할 점은 react-query ,swr 같은 경우는 서버 컴포넌트에서 작동하지 않기 때문에 'use client' 을 주입하여 클라이언트 컴포넌트에서 작동해야 합니다.

// src/app/ssg/page.tsx
import SsgView from '@/views/SsgPage';
import { getPosts } from '@/utils/getPosts';

export default async function Page() {
  const queryClient = getQueryClient();
  await queryClient.prefetchQuery(['ssg-posts'], () => getPosts());
  const dehydratedState = dehydrate(queryClient);

  return (
    <Hydrate state={dehydratedState}>
      <SsgView />
    </Hydrate>
  );
}

// src/view/SsgView.tsx
'use client';
import { useQuery } from '@tanstack/react-query';

import { type Posts } from '@/types/posts';
import { getPosts } from '@/utils/getPosts';

const SsgView = () => {
  const { data } = useQuery<Posts[]>(['ssg-posts'], () => getPosts());

  return (
		....
  );
};

export default SsgView;

참고
https://nextjs.org/docs![](https://velog.velcdn.com/images/qqww08/post/2e3548a1-10f4-4d3f-a8c8-358bbd546384/image.png)

https://codevoweb.com/setup-react-query-in-nextjs-13-app-directory/

profile
프론트엔드 개발자 입니다

0개의 댓글