리액트에서 비동기 로직, 대표적으로 api를 이용하기 위한 서버와의 데이터 통신 등을 리액트스럽게 다룰 수 있게 해주는 라이브러리이다.
많은 상태관리 라이브러리 중에서도 리액트 쿼리는 특히나 server state를 아주 효율적으로 관리할 수 있다. 기존에는 isLoading, isError, refetch, 데이터 캐싱 등을 개발자가 직접 구현하려면 꽤나 까다로웠던 반면, 리엑트 쿼리에서는 아주 간단하게 제공할 수 있다.
Server State: 세션간 지속되는 데이트를 말하며 비동기적이다. 세션을 진행하는 클라이언트에서만 소유되는 데이터가 아니다. 클라이언트에서는 서버 데이터의 스냅샷(특정한 시점에서 정지된 데이터)을 사용하기 때문에, 클라이언트에서 보이는 서버 데이터가 항상 최신이리라 보장할 수 없다.
예) 리액트 앱에서는 비동기 요청으로 받아올 수 있는 백엔드 DB의 데이터
Client State: 세션간 지속적이지 않은 데이터이다. 동기적이며, 클라이언트가 소유한다. 항상 최신 데이터로 업데이트되며 이는 렌더링에 반영한다.
예) 리액트 컴포넌트의 state, 동기적으로 저장되는 redux store의 데이터
Redux 같은 전역 상태관리 라이브러리들이 클라이언트 상태값에 대해서는 잘 작동하지만, 서버 상태에서는 그러지 않을 수 있다. 서버 데이터는 항상 최신 상태임을 보장하지 않기 때문이다. 네트워크 통신은 최소한으로 줄이는게 좋은데, 복수의 컴포넌트에서 최신 데이터를 받아오기 위해 fetching을 여러번 수행하는 낭비가 발생할 수 있다.
QueryClientProvider
를 최상단에서 감싸줘야 한다. client={queryClient}
를 작성해준다. import { QueryClient, QueryClientProvider } from 'react-query';
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<home />
<QueyrClientProvider>
);
}
이를 통해 앱에서 비동기 요청을 알아서 처리하는 background 계층을 이루게 된다.
import { useQuery } from 'react-query';
// 주로 사용되는 3가지 return 값 외에도 더 많은 return 값들이 있다.
const { data, isLoading, error } = useQuery(queryKey, queryFn, options)
QueryKey
를 기반으로 데이터 캐싱을 관리한다. useQuery('todos', ...) // 문자열
useQuery(['todos'], ...) // 배열
const { data, isLoading, error } = useQuery(['todos', id], () => axios.get(`http://.../${id}`));
useQuery(['todos', todoId], () => fetchTodoById(todoId));
혹은 이렇게도 표현할 수 있다.
useQuery(['todos', todoId], async () => {
const data = await fetchTodoById(todoId);
return data
});
다양한 옵션이 있지만, 자주 사용되는 옵션을 소개해본다.
enabled
는 쿼리가 자동으로 실행되지 않게 설정하는 옵션이다. const { data } = useQuery(['todos', id], () => fetchTodoById(id), {
enabled: !!id, // id가 존재할 때만 쿼리 요청을 한다는 의미의 코드이다.
});
retry
는 실패한 쿼리를 재시도하는 옵션이다. staleTime
은 데이터가 fresh
상태로 유지되는 시간이다. 해당 시간이 지나면 stale상태가 된다. cacheTime
은 inactive
상태인 캐시 데이터가 메모리에 남아있는 시간이다. 이 시간이 지나면 캐시 데이터는 가비지 컬렉터에 의해 메모리에서 제거된다. refetchOnMount
는 데이터가 stale 상태일경우 마운트될떄마다 refetch를 실행하는 옵션이다. refetchOnWindowFocus
는 데이터가 stale 상태일 경우 윈도우 포커싱될때마다 refetch를 실행하는 옵션이다. const { data: userInfo } = useQuery(['user'], getUser, {
refetchOnWindowFocus: true,
staleTime: 60 * 1000, // 1분
})
refetchOnReconnect
는 데이터가 stale 상태일 경우 재 연결될때 refetch를 실행하는 옵션이다. onSuccess
는 쿼리 성공시 실행되는 함수이다. onError
는 쿼리 실패 시 실행되는 함수이다. const { data: userInfo } = useQuery(['user'], getUser, {
refetchOnWindoFocus: true,
staleTime: 60 * 1000,
onError: (error) => {
if (error.response?.data.code === 401) {
//...
}
},
})
onSettled
는 쿼리가 성공해서 성공한 데이터가 전달되거나, 실패해서 에러가 전달될때 실행되는 함수이다. initialData
를 설정하면 쿼리 캐시의 초기 데이터로 사용된다. (쿼리가 아직 생성되지 않았거나 캐시되지 않았을 때)function App() {
const mutation = useMutation(newTodo => axios.post('/todos', newTodo))
return (
<div>
{mutation.isLoading ? (
'Adding todo...'
) : (
<>
{mutation.isError ? (
<div>An error occurred: {mutation.error.message}</div>
) : null}
{mutation.isSuccess ? <div>Todo added!</div> : null}
<button
onClick={() => {
mutation.mutate({ id: new Date(), title: 'Do Laundry' })
}}
>
Create Todo
</button>
</>
)}
</div>
)
}
useQuery와는 다르게 create, update, delete를 담당하며, server state에 사이드 이펙트를 일으키는 경우에 사용한다.
// 캐시가 있는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries()
// 'todos'로 시작하는 모든 쿼리들을 invalidate한다.
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries({
predicate: query =>
query.queryKey[0] === 'todos' && query.queryKey[1]?.version >= 10,
})
쿼리의 데이터가 요청을 통해 서버에서 바뀌었다면, 백그라운드에 남아있는 데이터는 과거의 것이 되어 앱에서 쓸모없어지는 상황이 발생할 수 있다.
invalidateQueries
메소드를 사용하면 개발자가 명시적으로 query가 stale되는 지점을 지정해줄 수있다. 해당 메소드가 호출되면 쿼리가 바로 stale되고, 리패치가 진행된다.
import { useMutation, useQueryClient } from 'react-query'
const queryClient = useQueryClient()
// 뮤테이션이 성공한다면, 쿼리의 데이터를 invalidate해 관련된 쿼리가 리패치되도록 만든다.
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})
mutation으로 요청 후 서버에서 받는 response값이 갱신된 새로운 데이터일 경우, mutation을 성공했을 때, 쿼리 데이터를 명시적으로 바꿔주는 queryClient 인스턴스의 setQueryData 메소드를 사용하면 좋다.
const queryClient = useQueryClient()
const mutation = useMutation(editTodo, {
onSuccess: data => queryClient.setQueryData(['todo', { id: 5 }], data),
})
mutation.mutate({
id: 5,
name: 'Do the laundry',
})
// 뮤테이션의 response 값으로 업데이트된 data를 사용할 수 있다.
const { status, data, error } = useQuery(['todo', { id: 5 }], fetchTodoByID)
Redux는 전역상태 관리를, React Query는 서버에서 받아온 데이터 관리를 하면서 역할을 분담할 수 있어, Redux 자체를 좀 더 취지에 맞게 사용할 수 있게 한다.
[React Query] 리액트 쿼리 시작하기 (useQuery)