다운로드 UI가 있을 때, 또는 UX를 저해시키는 불필요한 네트워크 요청을 제거하기 위해 사용된다.
대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을줄일 수 있다.
queryFn 의 매개변수로 Abort Signal을 받을 수 있고, 이를 이용해서 Query 취소를 가능하게 한다.
queryFn은 매개변수로 QueryFuntionContext란 객체를 받는다.
(onClick시 실행되는 함수가 자동으로 event 객체를 받는 것과 유사)
export const getTodos = async (queryFnContext) => {
const { queryKey, pageParam, signal, meta } = queryFnContext;
// queryKey: 배열형태의 쿼리키
// pageParam: useInfiniteQuery 사용 시 getNextPageParam 실행 시 적용
// signal: AbortSignal 을 의미 (네트워크 요청을 중간에 중단시킬 수 있는 장치)
// meta: query에 대한 정보를 추가적으로 메모를 남길 수 있는 string 필드
const response = await axios.get("http://localhost:5000/todos", { signal });
return response.data;
};
useQuery({
queryKey: ["todos"],
queryFn: getTodos,
})
// example: <div onClick={(event) => {}}
API 요청 시 기본 설정은 컴포넌트가 언마운트 되어도 네트워크 요청은 중단되지 않는다.
GET 요청 시 abort signal이 옵션으로 들어간 경우만 언마운트 시 자동으로 네트워크 취소가 된다.
import axios from 'axios'
const query = useQuery({
queryKey: ['todos'],
queryFn: ({ signal }) =>
axios.get('/todos', {
// Pass the signal to `axios`
signal,
}),
})
export const getTodos = async ({ signal }) => {
console.log("getTodos 호출");
const response = await axios.get("http://localhost:5000/todos", { signal });
return response.data;
};
const cancelQuery = () => {
queryClient.cancelQueries(["todos"]);
};
// 생략
<button onClick={cancelQuery}>todos 쿼리취소</button>
❓ 그럼 모든 GET 요청 마다 Abort Signal을 심으면 좋을까?
- 불필요한 네트워크 요청을 최소화 한다는 명분으로 단순하게 모든 GET 요청마다 Abort Signal을 심는 것은 작업부하를 올리기 때문에 바람직하지 않다. 동영상 다운로드 같은 대용랑 fetching 이 아닌 이상 대부분의 GET 요청은 빠르게 완료 및 캐싱처리 되어 성능에 유의미한 영향을 끼치지 못한다. 대용량 fetching 이 있는 경우 또는 Optimistic UI 를 구현할 때처럼 필요한 경우에만 적용하는 것을 권장한다.
(getTodo 가 실행된 후 빈 화면으로 이동
을 클릭하거나, todos 쿼리취소
버튼을 클릭하면 GET 요청이 cancel 된다.)
서버 요청이 정상적으로 잘 될거란 가정 하에 UI 변경을 먼저 하고, 서버 요청하는 방식이다. 혹시라도 서버 요청이 실패하는 경우, UI를 원상복구 (revert / roll back) 한다.
function Main() {
const navigate = useNavigate();
const {
isLoading,
isFetching,
data: todos,
} = useQuery({
queryKey: ["todos"],
queryFn: getTodos,
});
const queryClient = useQueryClient();
const addMutation = useMutation(addTodo, {
/*
노멀 업데이트
onSuccess: () => {
queryClient.invalidateQueries(["todos"]);
},
*/
// 낙관적 업데이트
onMutate: async (newTodo) => {
console.log("onMutate 호출");
// 일단 쿼리 취소를 먼저 한다.
await queryClient.cancelQueries({ queryKey: ["todos"] });
// 현재 캐시 데이터를 previousTodos에 백업한다.
const previousTodos = queryClient.getQueryData(["todos"]);
// prevTodo에 newTodo 합쳐서 캐시데이터에 할당 -> 리렌데링 되어 UI 먼저 바뀜
queryClient.setQueryData(["todos"], (old) => [...old, newTodo]);
// 리턴문 : onError의 3번째 인자인 context에 previousTodos 들어간다
return { previousTodos };
},
onError: (err, newTodo, context) => {
console.log("onError");
console.log("context:", context);
// addTodo가 실패하면 빠르게 previousTodos로 원상복구
queryClient.setQueryData(["todos"], context.previousTodos);
},
onSettled: () => {
// 실패하든 성공하든 결과가 오면
console.log("onSettled");
queryClient.invalidateQueries({ queryKey: ["todos"] }); // db에 있는 값 캐시데이터에 반영
},
});
const [content, setContent] = useState("");
const handleChange = (e) => {
setContent(e.target.value);
};
const handleSubmit = (e) => {
e.preventDefault();
addMutation.mutate({ content });
};
if (isLoading) {
console.log("Main return Loading");
return <h1>Loading...</h1>;
}
return (
<div>
<form onSubmit={handleSubmit} >
<input value={content} onChange={handleChange} />
<button>투두 추가</button>
</form>
페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출(prefetching) 한다.
캐시 데이터가 있는 상태로 해당 페이지로 이동 시 로딩 없이 바로 UI를 볼 수 있다.
const prefetchTodos = async () => {
// The results of this query will be cached like a normal query
// prefetch 할 queryKey와 queryFn 은 이동할 페이지의 쿼리와 동일해야 적절합니다.
await queryClient.prefetchQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
}
다른 페이지 클릭 시 매번 Loading UI를 보여주기 보다는 기존 UI 를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용할 수 있다.
useQuery의 옵션 중 keepPreviousData
를 true
로 바꾸면 이전 캐시데이터를 기반으로 isLoading 여부를 판단하게 한다.
const { data: movies, isLoading } = useQuery({
queryKey: ["movies", page], // initial queryKey:["movie", 1]
queryFn: fetchMovieData,
select: ({ total_pages, results }) => ({
total_pages,
results,
}),
keepPreviousData: true, // ⭐️ isLoading 을 스킵 가능. 이전 데이터 유지하라!
});