서버의 응답을 Store에 저장할 필요가 없는 상황에서도 구조화를 위해 Action Types, Actions, Reducer를 만들고, Action을 호출하는 등의 불필요한 상황, 캐싱 처리 및 관리 문제 등이 존재
비동기 통신을 위해 Store가 사용되어 Store의 본질과 다르게 규모가 비대해지는 경우가 발생한다. 기능이 추가될 수록 Store가 비대해진다.
Store에서 비동기 통신 분리하기 (feat. React Query) | 우아한형제들 기술블로그
Admin 페이지와 같은 관리형 페이지에서는 클라이언트의 전역 상태 데이터는 많이 필요하지 않을 수 있다. 이러한 페이지에서 Ducks 구조보다는 React Query를 적용하면 구조를 더 단순화 시킬 수 있다.
캐싱
- 데이터를 임시로 저장하여 다음에 필요할 때 빠르게 가져올 수 있도록 하는 기법
- 데이터를 요청할 때마다 API에 요청하지 않고, 이전에 저장해둔 데이터를 사용할 수 있어서 응답 속도가 빠름
- React Query는 캐싱 기능을 제공하여, 외부 API로부터 가져온 데이터를 캐싱하고 이를 재사용할 수 있음
현재 시점(2023년 1월 26일) 기준으로 가장 최신 버전의 React Query는 v4다. react-query라는 이름으로 설치받으면 v3이 설치된다. 가장 최신 버전인 v4를 설치받고 싶다면 **@tanstack/react-query로 다운받아야한다.**
TanStack Query | React Query, Solid Query, Svelte Query, Vue Query
npm i @tanstack/react-query
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from "react-query/devtools";
// Create a client
const queryClient = new QueryClient();
ReactDOM.render(
// Provide the client to your App
<QueryClientProvider client={queryClient}>
<Todos />
</QueryClientProvider>,
document.getElementById("root")
);
QueryClientProvider는 리액트 애플리케이션에서 비동기 요청을 처리하기위한 Context Provider로 동작하며 하위 컴포넌트에서 QueryClient를 사용할 수 있게 해준다.
useQuery | TanStack Query Docs
useMutations
을 사용해야함useQuery
를 사용하고 싶다면 useQueries
를 사용하는 것을 추천enabled
옵션을 사용하면 useQuery
를 동기적으로 사용 가능data
: fetch한 데이터. 데이터가 fetch될 때까지 undefined
의 값을 가진다.status
: 쿼리의 상태. "loading"
, "error"
, 혹은"success"
의 세가지 종류가 있다.error
: 발생한 오류. 발생한 오류가 없다면 undefined
의 값을 가진다.isFetching
: 쿼리가 fetching 중인지 여부를 나타내는 값으로 Boolean 값을 가진다.const Todos = () => {
const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList, {
refetchOnWindowFocus: false, // react-query는 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아오면 이 함수를 재실행합니다. 그 재실행 여부 옵션 입니다.
retry: 0, // 실패시 재호출 몇번 할지
onSuccess: data => {
// 성공시 호출
console.log(data);
},
onError: e => {
// 실패시 호출 (401, 404 같은 error가 아니라 정말 api 호출이 실패한 경우만 호출됩니다.)
// 강제로 에러 발생시키려면 api단에서 throw Error 날립니다. (참조: https://react-query.tanstack.com/guides/query-functions#usage-with-fetch-and-other-clients-that-do-not-throw-by-default)
console.log(e.message);
}
});
if (isLoading) {
return <span>Loading...</span>;
}
if (isError) {
return <span>Error: {error.message}</span>;
}
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
};
function Todos() {
const { status, data, error } = useQuery("todos", fetchTodoList);
if (status === "loading") {
return <span>Loading...</span>;
}
if (status === "error") {
return <span>Error: {error.message}</span>;
}
return (
<ul>
{data.map(todo => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
);
}
enabled
옵션을 사용enabled
를 넣으면 그 값이 true일 때 useQuery를 실행시킴const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const { data: nextTodo, error, isFetching } = useQuery(
"nextTodos",
fetchNextTodoList,
{
enabled: !!todoList // true가 되면 fetchNextTodoList를 실행한다
}
);
// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
const result = useQueries(
queries : [
{
queryKey: ["getRune", riot.version],
queryFn: () => api.getRunInfo(riot.version)
},
{
queryKey: ["getSpell", riot.version],
queryFn: () => api.getSpellInfo(riot.version)
}
]);
useEffect(() => {
console.log(result); // [{rune 정보, data: [], isSucces: true ...}, {spell 정보, data: [], isSucces: true ...}]
const loadingFinishAll = result.some(result => result.isLoading);
console.log(loadingFinishAll); // loadingFinishAll이 false이면 최종 완료
}, [result]);
const riot = {
version: "12.1.1"
};
const result = useQueries([
{
queryKey: ["getRune", riot.version],
queryFn: params => {
console.log(params); // {queryKey: ['getRune', '12.1.1'], pageParam: undefined, meta: undefined}
return api.getRunInfo(riot.version);
}
},
{
queryKey: ["getSpell", riot.version],
queryFn: () => api.getSpellInfo(riot.version)
}
]);
import { QueryCache } from '@tanstack/react-query'
const queryCache = new QueryCache({
onError: error => {
console.log(error)
},
onSuccess: data => {
console.log(data)
}
})
const query = queryCache.find({ queryKey: ['posts'] })
[find](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefind)
[findAll](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefindall)
[subscribe](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachesubscribe)
[clear](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycacheclear)
**[queryCache.find](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefind)
**
undefined
가 반환const query = queryCache.find({ queryKey })
**[queryCache.findAll](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachefindall)
**
const queries = queryCache.findAll({ queryKey })
**[queryCache.subscribe](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycachesubscribe)
**
const callback = event => {
console.log(event.type, event.query)
}
const unsubscribe = queryCache.subscribe(callback)
**[queryCache.clear](https://tanstack.com/query/v4/docs/react/reference/QueryCache#querycacheclear)
**
queryCache.clear()
useMutation | TanStack Query Docs
isIdle
or status === 'idle'
: mutation이 실행 되지 않아 아직 캐싱되지 않은 상태.isLoading
or status === 'loading'
: mutation이 실행중인 상태isError
or status === 'error'
: mutation에 에러가 발생한 상태isSuccess
or status === 'success'
: mutation이 성공적으로 실행되었고 데이터를 사용 가능한 상태능error
: mutation이 isError 상태인 경우 에러 정보 확인을 위해 사용하는 프로퍼티data
: mutation이 isSucess 상태인 경우 데이터 사용을 위해 사용하는 프로퍼티function App() {
const mutation = useMutation({
mutationFn: (newTodo) => {
return 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>
)
}
invalidateQueries
const mutation = useMutation(postTodo, {
onSuccess: () => {
// postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['reminders'] })
}
});
setQueryData
const queryClient = useQueryClient();
const mutation = useMutation(editTodo, {
onSuccess: data => {
// data가 fetchTodoById로 들어간다
queryClient.setQueryData(["todo", { id: 5 }], data);
}
});
const { status, data, error } = useQuery(["todo", { id: 5 }], fetchTodoById);
mutation.mutate({
id: 5,
name: "nkh"
});