React-Query를 사용하기 위해서는 먼저 다음의 기초 작업들이 필요하다.
npm i @tanstack/react-query
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient(); // QueryClient 인스턴스 생성
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<MainContent />
</QueryClientProvider>
);
}
useQuery란 React-Query 라이브러리의 hook으로, 주로 데이터를 조회할 때 사용된다. 데이터를 로딩중인 상태인지, 주기적으로 재요청을 설정할지 등 get 메소드로 fetch할 때 사용한다.
const { useQuery의 프로퍼티들 } = useQuery<data타입, 에러타입>({
queryKey: ['쿼리키', 쿼리 파라미터],
queryFn: () => api요청함수(쿼리 파라미터),
});
// 예시
const { data: todos, isLoading } = useQuery<getTodosRes[], AxiosError>({
queryKey: ['getTodos', selectedDate],
queryFn: () => getTodos(selectedDate),
});
* 쿼리키
: 특정 데이터 요청을 식별, 같은 쿼리키로 데이터를 캐시. 쿼리키가 같아도 쿼리 파라미터가 다르면, 서로 다른 쿼리키로 인식. 아래 두 useQuery훅은 서로 다른 쿼리키를 가짐.
e.g)
const date1 = '2024-09-21'
const { data: todos1 } = useQuery<getTodosRes[], AxiosError>({
queryKey: ['getTodos', date1],
queryFn: () => getTodos(date1),
});
const date2 = '2024-09-22'
const { data: todos2 } = useQuery<getTodosRes[], AxiosError>({
queryKey: ['getTodos', date2],
queryFn: () => getTodos(date2),
});
※ useQuery는 비동기적으로 작동하므로, 동시에 여러개의 쿼리를 작동시키려면 useQueries를 사용한다.
const results = useQueries({
queries: [
{ queryKey: ["post", 1], queryFn: fetchFunc, staleTime: Infinity },
{ queryKey: ["post", 2], queryFn: fetchFunc, staleTime: Infinity },
],
});
useQueries가 아니더라도 useQuery를 사용해서 동기적으로 처리를 할 수 있다.
먼저 1개의 데이터를 조회하고 그 데이터를 이용해 다른 데이터를 조회하는 방법은 다음과 같다.
const { data: data1 } = useQuery({
queryKey : ["data1"],
queryFn : getData1
});
const { data: data2 } = useQuery({
queryKey : ["data2", data1],
queryFn : () => getData2(data1),
enabled: !!data1,
});
이 때, data2의 쿼리파라미터로 data1을 지정해줌으로써, data1의 변화에 따라 쿼리가 재실행된다는 것이 핵심이다.
isLoading
: 데이터가 로딩 중인지 여부
fetching이 진행 중일 때 true
data
:
fetching이 완료되면 API에서 반환된 데이터가 저장
isFetching
:
데이터가 로딩 중이거나 다시 fetch 중인지 여부.
데이터를 새로 가져오고 있는 경우, true
isError
:
fetching 중 오류가 발생했는지 여부.
error
:
fetching 중 오류가 발생했을 경우 오류 정보를 포함.
refetch
:
쿼리를 수동으로 다시 fetch할 수 있는 함수.
특정 동작 (onClick, isError) 시 refetch를 하도록 할 수 있음.
dataPrevious
:
이전 fetch에서 가져온 데이터를 포함.
isSuccess
:
fetching이 성공적으로 완료되었는지 여부
dataUpdatedAt
:
데이터가 마지막으로 업데이트된 시간 반환
status
:
쿼리의 현재 상태를 나타내는 문자열.
loading, error, success 등이 포함됨.
캐시된 데이터를 사용하면 똑같은 데이터를 조회할때 불필요하게 fetch 하지 않아도 되고, 데이터의 요청 없이 빠르게 로드할 수 있는 이점이 있지만, 현재 캐시된 데이터가 최신 상태인지 모르는 상황도 있을 것이다.
그럼, 사용자가 A컴포넌트를 이용하다가 B컴포넌트를 이용하려고 할 때,
이 짧은 시간 사이에 새로운 데이터가 DB에 추가되었다면 B는 최신 데이터를 가져올 수 있는가?
결론부터 말하자면, staleTime에 따라 달라진다.
먼저, staleTime
이란, 캐시된 데이터가 최신 데이터로 인식되어져 있는 시간이다. default값은 0으로, 캐시된 데이터가 오래된 데이터라고 인식하고 useQuery를 사용할 때마다 새로운 fetch 요청을 보낸다. 반대로 staleTime : infinity
로 설정하면 캐시된 데이터를 항상 최신 상태라고 인식한다.
데이터를 요청한지 5초간은 최신상태로 인식하도록 설정하는 staleTime 설정 예시는 다음과 같다.
const { data } = useQuery({
queryKey: ['쿼리키'],
queryFn : api요청함수,
staleTime : 5000 // 5초후 stale 상태로 변경
});
이 경우는 5초의 시간이 지나면, 현재 데이터는 최신 상태가 아니라고 간주하게 된다. 즉, A 컴포넌트가 로드된 후, B로 이동하는 시간이 5초가 안된다면, B에서는 새로운 데이터를 요청하지 않고 캐시된 데이터를 사용할 것이다.
refetchOnMount
: 컴포넌트가 마운트 될 때, 최신 데이터를 가져오도록 한다. 이 때, 아직 staleTime이 지나지 않았다면, fetch를 요청하지 않는다.e.g )
const { data } = useQuery({
queryKey: ['쿼리키'],
queryFn : api요청함수,
refetchOnMount: true,
});
refetchInterval
: 주기적으로 최신 데이터를 요청하도록 한다. 너무 짧은 주기로 설정하면 백엔드 서버로의 트래픽이 늘어날 수 있다.e.g )
const { data } = useQuery({
queryKey: ['쿼리키'],
queryFn : api요청함수,
refetchInterval: 10000, // 10초마다 fetch
});
enabled
: enabled가 true 일 경우에만, 해당 쿼리가 실행되도록 한다. false일 경우 쿼리가 실행되지 않는다. default는 true이다.const { data } = useQuery({
queryKey: ['쿼리키'],
queryFn : api요청함수,
enabled: false,
});
/*
이 경우, 쿼리는 절대 실행 안됨.
enabled에 !!변수 를 이용해서 사용할 수 있음
변수를 쿼리파라미터로 사용(["queryKey", 변수])하면 useQuery는 변수의 변화를 감지해서 쿼리를 재실행
*/
// 예시)
// data1이 특정 데이터값을 가지면, 쿼리가 활성화됨
// data1이 아무 값이 없으면, 쿼리가 비활성화됨
const { data: data2 } = useQuery({
queryKey : ["data2", data1],
queryFn : () => getData2(data1),
enabled: !!data1,
});
invalidateQueries
: 현재쿼리의 staleTime을 즉시 만료된 상태로 한다. 즉, A의 종료함수로 설정하면 다음 컴포넌트에서 똑같은 쿼리 요청시 최신 데이터를 fetch한다.import { useQueryClient } from "react-query";
const AComponent = () => {
const queryClient = useQueryClient();
const { data } = useQuery({
queryKey: ['쿼리키'],
queryFn : api요청함수,
staleTime : 60000,
});
useEffect(() => {
return () => {
queryClient.invalidateQueries({
queryKey : ['쿼리키']
});
};
}, [queryClient]);
return <div>{data}</div>;
};
staleTime이 안지난다면 현재 캐시된 데이터를 최신상태로 인식하지만, useEffect의 종료함수에서 invalidateQueries를 사용하여, 컴포넌트 언마운트 시 캐시된 데이터가 만료된 상태로 바꾼다.
useQuery가 GET 메소드를 처리하는데에 주로 사용했다면, useMutation은 POST, PUT, DELETE 등의 데이터의 수정 작업에 사용하는 hook이다.
사용방법은 useQuery 훅에서 쿼리키를 생략할 수 있다는 것만 제외하면 비슷하다.
import { useMutation } from '@tanstack/react-query';
import axios from 'axios';
const postData = async (newData) => {
const response = await axios.post('/api/data', newData);
return response.data;
};
const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
})
return (
<div>
<button
onClick={() => {
mutation.mutate({ name: 'New Item' });
}}
>
Add Item
</button>
{mutation.isLoading && <p>로딩중</p>}
{mutation.isError && <p>에러 발생</p>}
{mutation.isSuccess && <p>요청 성공</p>}
</div>
);
};
useMutation은 다음과 같은 메소드가 있다.
mutation.isLoading()
: 요청이 진행 중인지의 여부
최신버전의 React-Query에서는 isPending()
으로 사용한다
mutation.isError()
: 요청이 실패했는지 여부
mutation.isSuccess()
: 요청이 성공했는지 여부
mutation.mutate(data)
: 매개변수로 수정할 data를 받아서 해당 요청(post,delete,put)을 수행.
isIdle()
: 아무 요청도 발생하지 않은 상태이면 true
const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
})
mutation.isLoading()
mutation.isError()
mutation.isSuccess()
mutation.mutate(데이터)
위처럼 사용해도 되고, 아래처럼 구조분해할당으로 사용해도 된다.
const {isLoading, isError, isSuccess, mutate : postData} = useMutation({
mutationFn : (데이터) => postData(데이터),
})
useMutation의 설정할 수 있는 옵션은 다음과 같다.
onSuccess
: 요청이 성공했을 때 실행할 콜백 함수 지정.const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
onSuccess: (data) => {
console.log("성공", data);
}
});
onSuccess의 경우 queryClient.invalidateQueries
와 조합하여 유용하게 사용할 수 있다.
const { data : todos } = useQuery("getTodos", getTodos) // #1 , #3
const postFunc = async (data) => {
// POST메소드
};
const queryClient = useQueryClient();
const { mutate : postData } = useMutation({
mutationFn : (데이터) => postData(데이터),
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: ["getTodos"]
});
},
});
postData({id : 1 , title : 할일제목}) // #2
return <div>{todos}</div> // #4
위 코드는 다음과 같이 동작한다.
1. getTodos 쿼리키를 가진 쿼리가, getTodos 콜백함수로 todos를 가져온다.
2. postData를 통해 data를 post하는 요청을 보낸다.
3. 성공적으로(onSuccess) post됐을 경우, getTodos 쿼리를 stale 상태로 설정하고, 데이터를 재요청한다.
4. todos가 업데이트되고, 업데이트된 todos가 렌더링된다.
onError
: 요청이 실패했을 때 실행할 콜백 함수 지정const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
onError: (error) => {
console.log("실패", error);
},
});
onSettled
: 요청 후에 무조건 실행되는 콜백 함수 지정const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
onSettled: (data, error) => {
console.log("요청 완료", { data, error });
},
});
onMutate
: mutation.mutate가 실행 직전에 실행되는 콜백 함수 지정. 여기서 variables는 mutation.mutate({데이터})
의 데이터에 해당함.const mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
onMutate: (variables) => {
console.log("요청 시작", variables);
return;
},
});
retry
: 요청이 실패했을 때 자동으로 재시도할 횟수 지정. default는 3이고, false 지정 시 재시도 Xconst mutation = useMutation({
mutationFn : (데이터) => postData(데이터),
retry: 2, // 실패 시 최대 두 번 재시도
});