우리는 다른 서버와의 API 통신과 비동기 데이터 관리를 위해 Redux-thunk
, Redux-saga
등의 미들웨어를 채택해서 사용했다. 그러나 다음과 같은 문제가 있다.
React-Query
는 서버의 값을 클라이언트에 가져오거나, 캐싱, 값 업데이트, 에러핸들링 등 비동기 과정을 더욱 편하게 하는데 사용하기 위한 라이브러리 이다!
여러가지 장점이 있지만, 주로 아래와 같은 이유로 사용한다.
invalidateQueries
)// v3
$ yarn add react-query
// v5
$ yarn add @tanstack/react-query
$ yarn add @tanstack/react-query-devtools
v4 부터 이름이 React-query 에서 TanStack Query로 바뀌었다. 사용 방법도 약간 다르지만 거의 유사하다.
앱의 최상단에 QueryClientProvider
를 통해 queryClient
를 제공해줘야 한다.
// src/index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
const queryClient = new QueryClient();
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
{/* devtools */}
<ReactQueryDevtools initialIsOpen={true} />
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById("root")
);
useQuery
의 리턴 값은 { isLoading, isError, data, error } 등이 들어있는 객체다.useQuery
는 비동기로 동작한다. 즉, 한 컴포넌트에 여러 개의 useQuery
가 있다면, 하나가 끝나고 다음 useQuery
가 실행되는 것이 아닌, 두 개의 useQuery
가 동시에 실행된다. 여러 개의 비동개 query가 있다면 useQueries
를 사용하기를 권장한다.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>
);
};
mutation 함수 선언
// src/api/mutiationFns
import axios from "axios";
const SERVER_URI = "http://localhost:4000";
export const addTodo = async (newTodo) => {
await axios.post(`${SERVER_URI}/todos`, newTodo);
};
useMutation 사용하여 데이터 수정
// imput.jsx
...
import { addTodo } from "../../../api/todos";
import { QueryClient, useMutation } from "react-query";
...
function Input() {
...
const queryClient = new QueryClient();
const mutation = useMutation(addTodo, {
onSuccess: () => {
// Invalidate and refresh
// 이렇게 하면, todos라는 이름으로 만들었던 query를 invalidate (무효화)할 수 있어요.
// addTodo가 성공하면 todos로 맵핑된 useQuery api 함수 실행⭐️
queryClient.invalidateQueries("todos");
},
});
...
const handleSubmitButtonClick = (event) => {
event.preventDefault();
// 생략
const newTodo = {
title,
contents,
isDone: false,
id: uuidv4(),
};
mutation.mutate(newTodo); // ⭐️ 여기
setTitle("");
setContents("");
};
[Invalidate의 과정]
v3에서는 useQuery를 사용할 때 첫 번째 인자(queryKey), 두 번째 인자(queryFn) 를 넣어주었다. 그리고 useMutation을 사용할 때는 첫 번째 인자(MutationFn)를 넣어주었다.
v5에서는 하나의 인자(객체) 에 이들을 모두 넣어주는 형식으로 약간 바뀌었다.
좀 더 명시적으로 바뀐 것 같다.
function Todos() {
// Access the client
const queryClient = useQueryClient()
// Queries
const query = useQuery({ queryKey: ['todos'], queryFn: getTodos })
// Mutations
const mutation = useMutation({
mutationFn: postTodo,
onSuccess: () => {
// Invalidate and refetch
queryClient.invalidateQueries({ queryKey: ['todos'] })
},
})
return (
<div>
<ul>
{query.data?.map((todo) => (
<li key={todo.id}>{todo.title}</li>
))}
</ul>
<button
onClick={() => {
mutation.mutate({
id: Date.now(),
title: 'Do Laundry',
})
}}
>
Add Todo
</button>
</div>
)
}
redux-thunk를 쓰면서 비효율적이라고 생각했던 부분을 react-query가 해결해 줄 수 있을 것 같다. 아직 익숙하지 않아서 좀 더 써보면서 익혀야 겠지만..