React-Query는 기존 Zustand나 Recoil 혹은 Redux와 같은 상태 관리 라이브러리에서 어려운 점들을 해소할 수 있도록 도와주는 라이브러리다. 기존의 상태 관리 라이브러리들은 충분히 훌륭하지만 서버측 상태관리를 하기엔 그다지 적합하지 않은 부분들이 있기 때문에 React-Query를 사용하게 된다.
예를 들어서 서버의 데이터를 변조한다던가 새로 데이터를 가져와야 할때 등 그런 상태들을 관리하게 해야 한다면 기존의 상태 관리 라이브러리들은 불편한 점들이 존재한다.
따라서 클라이언트측 상태 관리보다 데이터베이스에 저장되어 있는 데이터와 같은 서버 상태를 관리해야한다면 React-Query를 사용하는게 좋다.
import { QueryClient } from "@tanstack/react-query";
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: Infinity,
},
},
});
await queryClient.prefetchQuery({ queryKey: ["posts"], queryFn: fetchPosts });
"use client";
import React, { useState } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
type providerType = {
children: React.ReactNode;
};
const Provider = ({ children }: providerType) => {
const [queryClient] = useState(new QueryClient());
return (
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
{children}
</QueryClientProvider>
);
};
export default Provider;
공식문서 https://tanstack.com/query/v4/docs/react/reference/QueryClient
고유 키에 연결된 비동기 데이터 원본에 대한 선언적 종속성이다. 서버에서 데이터를 받아올 때 사용하는 메서드이다.
useQuery는 최소 두가지를 만족해야 한다.
import { useQuery } from "react-query";
function App() {
const info = useQuery("todos", fetchTodoList);
}
고유 키는 애플리케이션 전체에서 쿼리를 다시 가져오고 캐싱하고 공유하기 위해 내부적으로 사용 된다.
고유키는 문자열과 배열 두가지를 사용할 수 있다.
쿼리에서 불러오는 결과의 상태는 다음과 같이 불러올 수 있다.
function Todos() {
const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList);
if (isLoading) {
return <span>Loading...</span>;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
// We can assume by this point that `isSuccess === true`
return (
<ul>
{data.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
사용자에게 필요한 데이터를 미리 가져올 수 있다. prefetchQuery를 이용하여 사용자에게 필요한 데이터를 미리 전달이 가능하다.
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
await queryClient.prefetchQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
});
};
SSR을 대응하도록 하면 데이터를 미리 서버측에서 가져와서 클라이언트측으로 보내기 때문에 클라이언트 측에서는 빠른 로딩이 가능하다.
두가지 방법이 있는데 이 프로젝트에서는 Hydrate를 사용한 만큼 Hydrate를 사용하는 방법에 대해 설명하고자 한다.
시작하기전 Hydrate와 dehydrate에 대해 알아두고 시작하면 이해하기 쉽다.
hydrate는 사전적 단어로는 수분 공급이라는 뜻이며 Next.js에서는 먼저 정적인 페이지를 렌더링 한 후 동적인 페이지를 만드는 과정을 뜻한다. 반대로 dehydrate는 캐시의 동결된 표현을 생성하여 Hydrate를 사용할 수 있도록 처리해주는 과정을 의미한다. (정적인 페이지를 만들어서 hydrate 할 수 있게끔 처리한다는 의미)
관련 공식 문서: https://tanstack.com/query/v4/docs/react/reference/hydration
// app/getQueryClient.jsx
import { QueryClient } from "@tanstack/react-query";
import { cache } from "react";
const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;
import { dehydrate, Hydrate } from "@tanstack/react-query";
import getQueryClient from "../../getQueryClient";
export default async function HydrateDetail({ postId }: { postId: number }) {
const queryClient = getQueryClient();
// ["voteDetail",postId] query key를 기준으로 api 요청한 데이터를 queryClient에 넣는다.
await queryClient.prefetchQuery(["voteDetail", postId], async () => {
const response = await voteDetailFetch(postId);
return response.res;
});
// dehydratedState에 queryclient 상태를 정의하고(api로 받아온 정보가 들어가 있음)
const dehydratedState = dehydrate(queryClient);
return (
// client에서 hydrate를 받을 때 SSR 당시 만들어두었던 dehydratedState 를 받는다.
<Hydrate state={dehydratedState}>
<VoteDetailItem postId={postId} />
</Hydrate>
);
}
💡 TypeScript를 사용중이라면 비동기 서버 컴포넌트를 작성하면 유형 오류에 대해 불평합니다. 임시적으로 ***{/* @ts-expect-error Server Component */}***를 현재 작성중인 Hydrate 컴포넌트 부모에 작성해서 막을 수 있다.
그 결과로 아래 그림에서 볼 수 있듯이 왼쪽 페이지 내용을 react-query의 prefetching을 통해 api요청으로 가져온 데이터의 dehydrate 한 결과를 SSR된 페이지에서 볼 수 있다.
const VoteDetail = ({
params,
}: {
params: { detail: string; lng: Locale };
}) => {
return (
<div
className={
"VoteDetail mt-10 mb-10 h-full w-full rounded-2xl border px-[5%] shadow-md dark:border-border-dark "
}
>
{/* @ts-expect-error Server Component */}
<HydrateDetail postId={params.detail} />
<PicketArea />
<CommentListArea />
</div>
);
};
서버 렌더링 중 클라이언트 구성 요소 useQuery내에 중첩된 호출은 Hydrate의 state 속성에 있는 프리페칭 데이터에 액세스 할 수 있다.
위 경우엔 VoteDetailItem 컴포넌트에서 useQuery를 통해 이미 가져온 데이터에 접근 할 수 있다.
"use client";
const VoteDetailItem = ({ postId }: { postId: number }) => {
// 서버 컴포넌트에서 미리 가져온 데이터를 사용할 수 있다.
const { data } = useVoteDetailQuery({
queryKey: "voteDetail",
postId: postId,
});
const { data: voteCheck } = useVoteCheckQuery({
queryKey: "voteCheck",
postId: postId,
});
//...이하 생략
};
공식 문서에서는 쿼리의 일부만 미리 가져오고 다른 쿼리는 클라이언트에서 가져오도록 하는것을 추천하고 있다.
특정 쿼리에 대한 prefetchQuery를 추가하거나 제거하여 서버가 렌더링 하는것을 제어하라는 의미다.