[Next.js 13] Next.js 13 with react query

Sming·2023년 6월 26일
16
post-thumbnail

react-query와 서버 컴포넌트

제목을 nextjs13과 react-query라고 하였으나 사실 nextjs12에서도 이용하던 방법입니다.

initialData, hydrate를 이용한 방법이죠. react-query (현 @tanstack/react-query)의 공식문서에도 있는 react query와 nextjs13의 app router 사용을 함께 보시죠.

react-query를 사용할 일이..?

nextjs13의 공식문서를 보면 data fetching은 server component, interativge한 작업은 client component를 이용하라고 친절하게 ✅, ❌ 로 구분도 해주었습니다.

그렇지만 실제 개발을 하다보면 서버 컴포넌트에서만 data fetching을 하는것은 이상적인 상황에 불가합니다.

client 상태를 기반으로 fetch를 해야할때는 client component에서 fetch를 해주어야하죠. 대표적인 예시로 무한스크롤이 있습니다.

page가 늘어날때마다 api의 page param을 하나씩 늘려가면서 호출을 해야하는데 server component에서는 page라는 상태를 참조할 수 없기때문에 client에서 fetch를 하는것이 강제됩니다.

react-query의 hydrate를 사용하는 이유

그러면 그냥 client side에서 fetch하면 되는거 아닌가요? 맞습니다.

하지만 나머지 부분이 server component로 구현하고 무한 스크롤 부분만 client component에서 렌더링을 하게된다면 나머지는 모두 고정되어있지만 혼자 매우 깜빡이며 매우 늦게 불러오는것 처럼 보이게 됩니다.

사실은 다른 요소들이 너무 빠른것 뿐인데 말이죠.

그래서 client side에서 fetch를 하는데 첫 데이터에 대해서 이미 그려진 html처럼 보여지게 하기위해서 (ssr) react-query에서는 prefetchhydrate라는것을 제공해주었습니다.

react-query with nextjs13

이제 react-query와 nextjs13이 어떻게 같이 사용되는지 볼 것입니다.

먼저 queryClient를 초기화해주는 함수를 만듭니다.

// app/getQueryClient.jsx
import { QueryClient } from '@tanstack/react-query';
import { cache } from 'react';

export const getQueryClient = cache(() => new QueryClient());

server component에서는 useQueryClient 같은 훅을 이용할 수 없으니 다음과 같이 custom하게 만들어줘야 합니다.

// server component
export const HydratedReservationItemList = async () => {
  const queryClient = getQueryClient();
  await queryClient.prefetchInfiniteQuery(
    ['reservation', {sortType: D}],
    () => getReservationList('R', false),
  );

  const dehydratedState = dehydrate(queryClient);

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

이렇게 server component에 queryClient를 초기화 시킨후 prefetchQuery or prefetchInfiniteQuery(페이지네이션 or 무한스크롤)을 이용합니다.

그후 dehydrate, Hydrate component를 이용하면 ['reservation', {sortType: D}] 이 key의 초기값이 캐싱이 됩니다.

그래서 보통 api call을 하면 loading할때 undefined상태였다가 api가 성공하면 data를 가지고 있는 문제를 처음부터 undefined 상태가 없이 data를 초기화시킨 상태로 이용할 수 있습니다.

약간의 한계..?

react-query를 이용할때 대부분 상태가 변경될때마다 refetch를 위하여 query key를 이용하는데요.

하지만 Hydrate는 queryKey가 일치할떄만 동작을 합니다. 말그대로 그 query key에 cache를 해두는것이죠.

그렇기때문에 이러한 queryKey를 맞춰주기위하여 처음값을 하드코딩으로 설정할 필요가 있습니다. ['reservation', {sortType: D, type: 'all'}] 이런 방식으로 초기값을 설정을 해야 합니다.

그렇지만 만약 초기에 들어갈 수 있는 잠재적인 값이 여러개라면 어떨까요? 예를들어, 현재 type: 'all'로 되어있는것에 10개의 타입으로 입장을 할수있다면? ex) https://example.com?category=all, https://example.com?category=hotel...

이런경우에는 queryString을 통해서 여러가지 상태로 진입을 할 수 있는데 처음에 type: all로만 prefetch를 해둔다면 type: hotel, type: air 이런 값으로 들어올때는 prefetch가 되어있지 않기때문에 동일하게 loading을 하다가 ui가 보이게 될것입니다.

물론 2,3개 정도의 작은 진입점이라면 그것들도 모두 prefetch해주면 됩니다. 간단하게 prefetchQuery를 2번, 3번 이용하면 되는것이죠.
하지만 10개, 20개 이렇게 된다면 물론 prefetchQuery를 10번, 20번으로 가능은 하겠지만 그렇다면 사용자가 새로고침할때마다 불필요한 api호출이 10개, 20개나 더 발생한다는것이죠.

이는 사용자가 많아지면 많아질수록 nextjs server에 더 많은 부담을 줄것이며 오히려 csr을 이용하는것이 나은 속도가 나올수도 있습니다.


이부분은 특정 진입점들은 사용자들이 많이 접근하겠다 하는것과 그 진입점의 갯수에 대해서 어디까지 prefetch를 이용할까는 스스로 고민하여 이용하시면 될것 같습니다.

최적화라는것에는 항상 트레이드오프가 따라온다는것만 명심하고 이용하면 큰 문제없이 이용할 수 있을것 같습니다.

결론

사실 사용하는 방법을 제외하면 next13이전 버전에도 동일하게 해당하는 내용들입니다. ssr인데 react-query를 이용하실분들은 다음과 같은 방법을 해보시고, prefetch를 어디까지 할지는 직접 고민하여 설정하시는 것을 추천합니다.

이것외에도 react-query를 이용하신다면 optimistic update라는 기술도 알아보시는 것을 추천합니다.

profile
딩구르르

3개의 댓글

comment-user-thumbnail
2023년 8월 24일

안녕하세요 글 잘 읽었습니다.
export const getQueryClient = cache(() => new QueryClient()); 이 부분에서 cache를 사용한 이유에 대해서 알고 싶습니다.

1개의 답글
comment-user-thumbnail
2023년 12월 10일

구글링을 하다 이글을 보게됬습니다.
아직 experimental 이긴 하지만 곧 정식버전에 포함될것으로 보이는(추측)
@tanstack/react-query-next-experimental 을 발견했는데요.

https://tanstack.com/query/v5/docs/react/guides/advanced-ssr#experimental-streaming-without-prefetching-in-nextjs

<ReactQueryStreamedHydration /> 이라는 프로바이더와 "useSuspenseQuery" 훅을 이용하면 서버컴포넌트로 react-query를 이용할수 있고, prefetchQuery 없이 훅을 이용하고 있음에도 경고 메세지 없이 잘 동작을 하네요.
깃헙을 보니 약 3개월 전쯤에 추가가 된듯 합니다.

답글 달기