
얼마전에 상태 관리 라이브러리인
recoil에 대해 학습했는데, 다른 라이브러리에 대해서도 궁금증이 생겼다. 그 중react query도 있다는 것을 알게 되었고 카카오와 같은 회사들에서 적극 도입했다는 것을 들었고 다양한 회사에서react query를 경험해본 사람을 선호한다는 내용을 보고 학습해보도록 했다.
React Query는 data fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어주는 리액트 라이브러리이다.Redux, Recoil, Mobx는 클라이언트 데이터를 관리하기에는 적합하나 서베 데이터를 관리하기에는 적합하지 않다Redux를 활용하여 프로젝트의 전역상태를 관리를 할때 서버 데이터를 활용하려면 Redux-saga, Redux-Thunk 혹은 RTK-Query같은 또 다른 미들웨어를 사용해야한다.fetching : 데이터 요청 상태fresh : 데이터가 프레시한 상태 (만료되지 않은 상태)fetching한다.stale("케케묵은"이라는 형용사) : 데이터가 만료된 상태fresh에서 stale로 넘어가는 시간 (기본값 : 0)inactive : 사용하지 않는 상태delete 가비지 콜렉터에 의해 캐시에서 제거된 상태import axios from 'axios';
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
}from 'react-query';
/*
리액트 쿼리는 내부적으로 queryClient를 사용하여
각종 상태를 저장하고, 부가 기능을 제공한다.
*/
const queryClient = new QueryClient();
function App(){
return(
<QueryClientProvider client = {queryClient}>
<Menus/>
</QueryClientProvider>
)
}
function Menus(){
const queryClient = useQueryClient();
// /menu api에 get요청을 보내 서버 데이터를 가져온다.
const { data } = useQuery('getMenu', () => {
axios.get('/menu').then({data} => data),
});
// /menu api에 post 요청을 보내 서버데이터를 저장한다.
const { mutate } = useMutation((suggest) => axios.post('/menu', {suggest}),{
/*
post 요청이 성공하면 위 useQuery의 데이터를 초기화한다.
데이터가 초기화되면 useQuery는 서버의 데이터를 다시 불러온다.
*/
onSuccess : () => queryClient.invalidateQueries('getmenu'),
});
return (
<div>
<h1> Tomorrow's Lunch Candidates! </h1>
<ul>
{data.map((item) => (
<li key={item.id}> {item.title} </li>
))}
</ul>
<button
onClick={() =>
mutate({
id: Date.now(),
title: 'Toowoomba Pasta',
})
}
>
Suggest Tomorrow's Menu
</button>
</div>
);
}
const { data } = useQuery(
queryKey, // (required) query 요청에 대한 응답 데이터를 캐시할 때 사용할 유니크 키
fetchFn, // (required) query 요청을 수행하기 위한 Promise를 return 하는 함수
options
); // (optional) useQuery에서 사용되는 옵션 객체
useMutation사용useQuery는 비동기로 작동한다.useQuery가 있다면 하나가 끝나고 다음 useQuery가 실행되는 것이 아닌 두 개의 useQuery가 동시에 실행된다. 단, 여러개의 비동기 쿼리가 있으면 useQueries가 더 좋다.enabled를 사용하면 useQuery를 동기적으로 사용 가능하다.const { status, data, error, isFetching, isPreviousData } = useQuery(
["projects", page],
() => fetchProjects(page),
{ keepPreviousData: true, staleTime: 5000 }
);
// 예외처리는 reject 대신 무조건 throw Error 처리
const { error } = useQuery(["todos", todoId], async () => {
if (somethingGoesWrong) {
throw new Error("err");
}
return data;
});
idle : 쿼리 data가 하나도 없고 비었을 때, {enabled : false} 상태로 쿼리가 호출되었을 때 이 상태로 시작됨loading : 로딩 중일 때error : 에러 발생했을 때success : 요청 성공했을 때enabled : True로 설정하면 자동으로 쿼리의 요청 함수가 호출되는 일이 없다.keepPreviousData : success와 loading 사이 널뛰기 방지placeholderData : mock 데이터 설정도 가능. 그러나 캐싱이 안된다.initialData : 초기값 설정enabled옵션을 사용하면 useQuery를 동기적으로 사용 가능하다.const { data: todoList, error, isFetching } = useQuery("todos", fetchTodoList);
const {
data: nextTodo,
error,
isFetching,
} = useQuery("nextTodos", fetchNextTodoList, {
enabled: !!todoList, // true일 때 fetchNextTodoList 실행
});
const usersQuery = useQuery("users", fetchUsers);
const teamsQuery = useQuery("teams", fetchTeams);
const projectsQuery = useQuery("projects", fetchProjects);
// useQuery를 하나로 묶을 수 있는데, 하나의 배열에 각 쿼리에 대한 상태 값이 객체로 들어온다.
// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
const result = useQueries([
{
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]);
mutation.reset : 현재의 error와 data를 모두 지울 수 있다.import { useState, useContext, useEffect } from "react";
import loginApi from "api";
import { useMutation } from "react-query";
const Index = () => {
const [id, setId] = useState("");
const [password, setPassword] = useState("");
const loginMutation = useMutation(loginApi, {
onMutation: (variable) => {
console.log("onMutate", variable);
// variable : { loginId : 'xxx', password : 'xxx' }
},
onError: (error, variable, context) => {
//error
},
onSuccess: (data, variables, context) => {
console.log("success", data, variables, context);
},
onSettled: () => {
console.log("end");
},
});
const handleSubmit = () => {
loginMutation.mutate({ loginId: id, password });
};
return (
<div>
{loginMutation.isSuccess ? "success" : "pending"}
{loginMutation.isError ? "error" : "pending"}
<input type="text" value={id} onChange={(e) => setId(e.target.value)} />
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button onClick={handleSubmit}>로그인</button>
</div>
);
};
export default Index;
invalidateQueries에 넣어주면 된다.const mutation = useMutation(postTodo, {
onSuccess: () => {
// postTodo가 성공하면 todos로 맵핑된 useQuery api함수를 실행한다.
queryClient.invalidateQueries("todos");
},
});
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",
});
queryClient.invalidateQueries();
queryClient.invalidateQueries("todos");
queryClient.invalidateQueries({
predicate: (query) =>
query.queryKey[0] === "todos" && query.queryKey[1]?.version >= 10,
});
recoil은 useState와 비슷한 느낌이어서 쉽게 이해하고 적용할 수 있었는데, 생각보다 리액트 쿼리는 좀 더 복잡한 것 같다. 실제로 적용해보면서 부딪혀보는 것이 학습하는 것에 도움이 될 것이라고 생각한다.