react-query는 react application에서 데이터를 가져오고 캐시하며 업데이트하는 과정 을 간소화 하는 상태 관리 라이브러리입니다. 즉 서버 상태(Server State)를 관리하는 라이브러리입니다.
💡 Client Sate vs. Server State
클라이언트 상태(Client State)와 서버 상 태(Server State)는 전혀 다른 개념이며, 클라이언트에서 소유하며 항상 클라이언트 내에서 사용자 인터렉션에 따라 최신으로 관리됩니다. 이에 반해 서버 상태(Server State)는 Client에서 제어하거나 소유하지 않으며, 데이터베이스에 저장되어 있는상 태라 할 수 있습니다.
자동 백그라운드 재요청, 캐싱 및 오래된 데이터 관리, 에러 처리, 페이지네이션, 무 한 스크롤 등을 간소화 해 구현할 수 있게 하는 장점이 있습니다.
import axios from "axios";const customFetch = axios.create({baseURL: "http://localhost:5001/api/tasks",});export default customFetch;
// HTTP GET 예시const fetchTasks = async () => {try {const response = await customFetch.get("/");console.log(response.data);} catch (error) {+console.error(error);}};useEffect(() => {fetchTasks();}, []);
위와 같이 useEffect로 요청을 설정하는 것과 비교해 react query는 불필요한 재렌더 링이나 네트워크 요청을 최소화 해 boilerplate 코드를 줄이고 성능을 향상시킵니다.
npm i @tanstack/react-query
react-query를 설치합니다.
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";const queryClient = new QueryClient();ReactDOM.createRoot(document.getElementById("root")).render(<QueryClientProvider client={queryClient}><App /></QueryClientProvider>);
react-query를 사용하려면 QueryClientProvider
로 최상단에서 감싸주어야 합니다.
그 후 QueryClient
인스턴스를 App 전체에 연결시켜야 합니다.
import { useQuery } from "@tanstack/react-query";const result = useQuery({queryKey: ["tasks"],queryFn: () => customFetch.get("/"),});console.log(result);
useQuery는 3개의 인자를 받습니다. 첫 번째 인자는 queryKey
, 두 번째 인자는
queryFn
로 필수입니다. 그러나 세 번째 인자 options
는 선택적입니다.
queryKey
는 데이터 캐싱을 관리하는 고유한 키입니다. 배열 표기법을 사용합니다.
애플리케이션 전체에서 쿼리를 공유하며 내부적으로 네트워크를 다시 요청하고 캐시합
니다. queryFn
은 Promise를 반환하는 함수를 넣어야 합니다. useQuery 훅이 호출될
때 실행되 는 콜백 함수입니다. 세 번째 인자는 다양한 옵션이 있는데 statleTime,
cacheTime, retry 등이 있습니다. useQuery
공식문서를 통해자
세히 살펴볼 수 있습니다. 위 예시에서는 queryKey와 queryFn만 사용해 보겠습니다.
const { mutate: createTask, isLoading } = useMutation({mutationFn: (taskTitle) => customFetch.post("/", { title: taskTitle }),onSuccess: () => {// 성공했을 때},onError: () => {// 실패했을 때},});
서버의 data를 post, patch, put, delete와 같이 수정 또는 삭제 하고자 할 때 useMutation을 사용합니다. useMutation은 비동기적으로 data를 수정하고, data를 업 데이트 해 UI를 재렌더링 합니다. useMutation hook의 반환 값인 mutation 객체는 mutation과 상호 작용하고 결과를 처리하는 데 사용할 수 있는 여러 프로퍼티와 메서 드가 있습니다.
Query Invalidation은 react-query에서 캐시된 data를 무효화 합니다. data를 수정, 삭제했을 때 해당 데이터를 refetch 해서 캐시를 업데이트 해야 합니다. react-query 는 Query Invalidation을 자동으로 처리하며 useMutation hook을 사용해 data를 수정, 삭제하면 해당 data를 가져오는 모든 query를 무효화합니다.
const { mutate: createTask, isLoading } = useMutation({mutationFn: (taskTitle) => customFetch.post("/", { title: taskTitle }),});const handleSubmit = (e) => {e.preventDefault();createTask(newItemName);};
data를 create 할 때에는 useMutation hook을 사용합니다. useMutation의 반환 값인
mutation 객체의 mutate
메서드를 이용해 요청 함수를 호출합니다.
const Items = () => {const { isLoading, data, error, isError } = useQuery({queryKey: ["tasks"],queryFn: async () => {const { data } = await customFetch.get("/");return data;},});// 쿼리가 현재 로딩 중이면if (isLoading) {return <p style={{ marginTop: "1rem " }}>Loading...</p>;}// 쿼리 도중 에러가 발생한 경우if (error) {return <p style={{ marginTop: "1rem " }}>{error.message}</p>;}return (<div className="items">{data.taskList.map((item) => {return <SingleItem key={item.id} item={item} />;})}</div>);};export default Items;
react-query를 통해 서버에서 data를 GET할 때에는 useQuery hook을 사용합니다.
useQuery hook은 객체를 반환합니다. 위 예시에서는 isLoading
, data
, error
,
isError
프로퍼티를 구조 분해 할당을 통해 사용했습니다.
GET 요청을 통해 data가 잘 fetch 된 것을 thunder client를 통해 확인할 수 있습니다
.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";// useQueryClient hook을 통해 queryClient 객체 생성const queryClient = useQueryClient();const { mutate: editTask } = useMutation({mutationFn: ({ taskId, isDone }) => {return customFetch.patch(`/${taskId}`, { isDone });},onSuccess: () => {// 'tasks' 키가 있는 모든 쿼리 무효화queryClient.invalidateQueries({ queryKey: ["tasks"] });},onError: (err) => {console.log(err);},});
data를 수정(edit)할 때에는 useMutation hook을 사용합니다. useMutation의 반환 값
인 mutation 객체의 mutate
메서드를 이용해 요청 함수를 호출합니다. mutate는
onSuccess
메서드를 통해 성공 했을 시 response data를 핸들링할 수 있습니다. 위
코드에서는 data가 수정되었으므로 캐시 data를 최신화 해야 합니다. 이 경우 쿼리가
오래되었다고 판단하고 다시 refetch 하기 위해 invalidateQuries()
메서드를 사용
합니다. onError
메서드를 통해 실패 했을 시 response data를 핸들링합니다.
const { mutate: deleteTask, isLoading } = useMutation({mutationFn: (taskId) => {return customFetch.delete(`/${taskId}`);},onSuccess: () => {// 'tasks' 키가 있는 모든 쿼리 무효화queryClient.invalidateQueries({ queryKey: ["tasks"] });},onError: (err) => {console.log(err);},});
data 삭제도 수정도 마찬가지로 mutate 메서드를 실행 후 성공했을 때(onSuccess) queryClient.invalidateQueries()를 통해 쿼리의 캐시된 data를 업데이트 시켜줍니다.