이전 프로젝트에서는 axios와 fetch를 사용해서 비동기를 구현하였다. 리액트로 개발을 하면서 좀 더 다양한 라이브러리를 사용하기 위해 이번 프로젝트에서는 React Query를 사용할 예정이다.
React Query란
서버 상태 관리 라이브러리로 비동기 로직을 쉽게 다루게 해준다.
import React from "react";
import ReactDOM from "react-dom/client";
import {
QueryClient,
QueryClientProvider,
useQuery,
} from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";
import axios from "axios";
const queryClient = new QueryClient();
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
);
}
function Example() {
const { isLoading, error, data, isFetching } = useQuery({
//캐싱하고 다시 불러오기 위한 키 값
queryKey: ["repoData"],
//queryKey: "repoData",
//콜백 함수를 실행시킨 값을 캐싱해서 저장하고 hook처럼 사용 가능
queryFn: () =>
axios
.get("https://api.github.com/repos/tannerlinsley/react-query")
.then((res) => res.data),
},
{
refetchInterval: 5000, //옵션값 - 5초마다 다시 불러오는 기능
});
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{" "}
<strong>✨ {data.stargazers_count}</strong>{" "}
<strong>🍴 {data.forks_count}</strong>
<div>{isFetching ? "Updating..." : ""}</div>
<ReactQueryDevtools initialIsOpen />
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.createRoot(rootElement).render(<App />);
const { data, isError, isLoading, error } = useQuery(queryKey, queryFn, options)
const { isLoading, error, data, isFetching } = useQuery({
queryKey: ["repoData"],
queryFn: () =>
axios
.get("https://api.github.com/repos/tannerlinsley/react-query")
.then((res) => res.data),
},
{
refetchInterval: 5000, //옵션값 - 5초마다 다시 불러오는 기능
efetchOnWindowFocus: false, // 사용자가 사용하는 윈도우가 다른 곳을 갔다가 다시 화면으로 돌아와서 함수 재실행 여부 옵션
//default 값: true
retry: 0, // 실패한 쿼리 재시도 옵션 default로 3번 재시도
//true=무한 재시도, false=재시도 X
staleTime: 60000, // 1분 동안 데이터가 fresh 상태로 유지됨. 이후에는 stale 상태
//fresh 상태: 쿼리가 다시 mount 되어도 fetch 실행X
//default: 0
cacheTime: 300000, // 5분 동안 inactive 상태인 캐시 데이터가 메모리에 남아있음 이후 메모리에서 제거
//default 값 = 5분
refetchOnMount: 'always', // stale 상태일 경우 매번 마운트 시 refetch 실행
//defualt=true
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);
}
});
const { isLoading, isError, data, error } = useQuery("todos", fetchTodoList)
const { status, data, error } = useQuery("todos", fetchTodoList);
if (status === "loading") {
return <span>Loading...</span>;
}
if (status === "error") {
return <span>Error: {error.message}</span>;
}
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);
// 아래 예시는 롤 룬과, 스펠을 받아오는 예시입니다.
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]);
값을 바꿀 때(update나 post) 사용하는 api이다.
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, {
onMutate: 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;
const mutation = useMutation(postTodo, {
onSuccess: () => {
// postTodo가 성공하면 todos로 맵핑된 useQuery api 함수를 실행합니다.
queryClient.invalidateQueries("todos");
}
});
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"
});
// src/index.js
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
suspense: true
}
}
});
ReactDOM.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</React.StrictMode>,
document.getElementById("root")
);
const { data } = useQurey("test", testApi, { suspense: true });
return (
// isLoading이 true이면 Suspense의 fallback 내부 컴포넌트가 보여집니다.
// isError가 true이면 ErrorBoundary의 fallback 내부 컴포넌트가 보여집니다.
<Suspense fallback={<div>loading</div>}>
<ErrorBoundary fallback={<div>에러 발생</div>}>
<div>{data}</div>
</ErrorBoundary>
</Supense>
);