
프론트에서는 데이터를 전역적으로 공유하는 상태 관리 라이브러리들이 있지만..(recoil, rtk, zustand) 이 라이브러리들은 클라이언트에서 공유되는 데이터 중심이다.
서버에서 가져오는 데이터의 경우 비동기적으로 이루어져 더 복잡한데 이를 효과적으로, 쉽게 관리해주는 것이 이 리액트 쿼리 이다.
const {data, error} = useQuery({queryKey: ['todo'],queryFn: getTodos})
queryKey : 쿼리의 이름을 나타낸다. 데이터를 캐싱,재요청, 공유 할때 사용되는 중요한 키이다. queryFn : 데이터를 패칭하는 함수이다. queryOption : 쿼리의 옵션을 지정합니다. 쿼리키를 지정하는 방법 : 배열
useQuery({queryKey : ['todo',todoId ,{preview: true}]})
배열에서 순서마저도 다른 쿼리키를 나타내기 때문에, 해당 쿼리키를 동일하게 사용하는 것이 중요하다!
페이지네이션을 구현할 경우 다음과 같이 queryKey에 page를 넣으면 된다.
const result = useQuery({
queryKey: ['projects', page],
queryFn: fetchProjects,
})
프로미스를 반환하는 하수면 뭐든 가능하다. 하지만 데이터를 반환하거나, 에러를 반환하는 동작을 꼭 수행해야 한다.
fetch의 경우 error를 기본적으로 내지 않기 때문에 error를 반환하는 로직이 필요하다. (axios의 경우에는 괜춘)
데이터 유효 시간 설정, 패칭 조건, 재실행 횟수 등 다양한 옵션들을 지정 가능하다. 데이터 시간 관련 옵션은 다음과 같이 지정한다.
export default function FollowingPosts() {
const { data } = useQuery({
queryKey: ["posts", "follow"],
queryFn: getFollowingPosts,
staleTime: 60 * 1000, //1분동안 fresh 함
gcTime: 300 * 1000,
//5분이 기본. 가비지 타임, 메모리에 저장되는 시간, 이 시간이 넘어가면 메모리에서 삭제된다. 즉, 캐시가 사라진다.
//staleTime보다 gcTime이 길어여 한다.
//inactive ; 현재 보는 화면에서 해당 데이터를 안쓰는 경우
});
원래 5v 전까지는 cacheTime을 썼지만, 5v에서부터는 gcTime으로 이름이 바꿔었다. 해당 시간이 지나면 garbage Collecte로 삭제된다는 뜻이다.
➡️ staleTime : 데이터가 fresh에서 stale로 변경되는 시간이다. 데이터의 유통기한. 유통기한이 지나면 오래된 데이터로 판단하여 데이터를 새롭게 fetch해야 한다고 인식한다.
fresh 상태일 때는 쿼리 인스턴스가 새롭게 mount되어도 데이터 fetch가 일어나지 않는다.
enabled를 false로 지정하면 자동적인 쿼리의 기능이 멈춘다. 예를 들어, 쿼리는 자동으로 데이터 패칭을 수행하지 않고, invalidateQueries,refetchQueries 같은 자동 패칭 함수를 무시한다.
쿼리에서 나오는 refetch 함수로 데이터를 패칭할 수 있다.
function Todos() {
const { refetch} = useQuery({
queryKey: ['todos'],
queryFn: fetchTodoList,
enabled: false,
})
return (
<div>
<button onClick={() => refetch()}>Fetch Todos</button>
...)}
데이터를 자동 요청하는 건 싫고, 원할 때만 데이터를 요청하고 싶을 때 유용한 기능인 것 같다. 다음과 같이 enable에 boolean을 넣으면 해당 값이 없을 때에는 데이터를 요청하지 않는다.
enabled: !!id
retry는 데이터 요청이 실패 했을 때 데이터 재요청을 몇번 수행할 건지 정하는 옵션이다. 디폴트는 3이다.
retry 횟수를 올리면 마냥 좋을 것 같지만, 횟수가 올라갈 수록 서버의 부하도 증가하기 때문에 적당한 횟수를 정해야 한다.
const result = useQuery({
queryKey: ['todos', 1],
queryFn: fetchTodoListPage,
retry: 10, // Will retry failed requests 10 times before displaying an error
})
재요청 하는 시간의 텀을 정할 수도 있는데 다음과 같이 정할 수 있다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
},
},
})
당연히 defaultOption이 아니라 쿼리의 option으로도 정할 수 다.
이 외에 다양한 옵션은 공식문서에 자세히 나와 있다 .
https://tanstack.com/query/latest/docs/react/reference/useQuery
Mutations
const mutation = useMutation({
mutationFn : postTodo,
onSuccess: () => {
//캐싱된 데이터를 무효화하고 refetch한다.
queryClient.invalidateQueries({queryKey : ['todos']})
}
})
useQuery는 여러 번 동시에 사용해도 전혀 문제가 없는데, useQueries가 이를 입증한다. 다음과 같이 배열에 담긴 유저 아이디를 map으로 순회해서 데이터 패칭 요청을 연속적으로 해도 아무 문제가 없다.
function app({user}){
const userQuries = useQueries({
queries : users.map((user) => {
return {
queryKey : ['user', user.id],
queryFn : () => fetchUserById(user.id),
}
})
})
}
유저가 창을 나갈 때도, 리액트 쿼리는 stale된 데이터를 자동 패칭한다. 이것이 낭비라고 여겨진다면 이 옵션을 다음과 같이refetchOnWindowFocus : false로 끌 수도 있다.
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false, // default: true
},
},
})
queryClient에서 defaultOption을 주면 모든 쿼리에서 자동으로 해당 옵션이 설정된다.
페이지네이션을 리액트 쿼리로 구현하면, 페이지마다 새로운 요청을 보내기 때문에, UI에 깜빡임 등이 발생할 수 있다.
그전에 데이터를 유지해 새로운 데이터가 들어올 때 이를 전환하여 UI를 개선시킬 수 있는데 이것이 placeholderData의 목적이다. 다음 링크에 좋은 예제가 있다.
리액트 쿼리는 자체적으로 타입 추론을 하는데, 다음과 같이 데이터를 요청하는 로직에 타입을 선언해주어야 한다. 데이터를 반환하는 함수에 직접 반환값의 타입을 지정하면, 그 함수를 queryFn으로 사용하는 쿼리가 알아서 데이터 타입을 추론한다.
const fetchGroups = ():Promise<Group[]> =>
axios.get('/group').then((res) => res.data);
const {data} = useQuery({queryKey : ['groups'], queryFn : fetchGroups})
다음과 같이 쿼리에 직접 타입을 줘도 된다. 쿼리만 봐도 어떤 타입의 데이터를 반환하는지 알 수 있어서 좋은 것 같다.
const fetchGroups = ():Promise<Group[]> =>
axios.get('/group').then((res) => res.data);
const {data} = useQuery<Group[],Error,Group[]>({queryKey : ['groups'], queryFn : fetchGroups})
useQuery의 타입
export function useQuery<
TQueryFnData = unknown, //쿼리(queryFn)에서 반환하는 타입
TError = Error, //queryFn에서 예상되는 오류 타입(error)
TData = TQueryFnData //데이터 프로퍼티가 최종적으로 보유되는 타입, 보통은 TQueryFnData과 같은 타입을 지정
TQueryKey extends QueryKey = QueryKey //queryFn에 전달된 queryKey를 사용하는 경우 해당 Querykey의 타입
>
두번째 error의 타입은 원래 unknown이었는데 v5에서 Error 타입으로 변경되었다. query에서 error를 꺼내 쓸때도 Error 타입이다.

const { error } = useQuery({ queryKey: ['groups'], queryFn: fetchGroups })
다음과 같은 화면이 뜨고 queryKey를 통해 데이터를 관리할 수 있다.

데이터 상태로는 다음과 같은 5가지 상태가 있다.
fresh : 데이터가 신선한(유효한 상태), staleTime이 지나지 않았을 때 이다
fetching: 데이터가 패칭되고 있는 상태, 매우 짧다.
pause : 쿼리가 일시정지 중인 상태, 네트워크가 좋지 않거나 하는 문제 등으로 인해 발생한다.
stale: 데이터가 유효하지 않은 상태. 쿼리가 재요청을 보낼 것이다.
inactive : 해당 쿼리 데이터가 현재 페이지에서 사용되고 있지 않은 상태. 쿼리는 stale된 데이터라도 inactive하면 재요청을 보내지 않는다(똑똑하넹)

https://tanstack.com/query/latest/docs/react/reference/useQuery
https://tanstack.com/query/latest/docs/react/guides/queries (가장 중요한 것은 공식문서)
https://velog.io/@dev_jazziron/useQuery
https://wonsss.github.io/library/tanstack-query-v5/