[TIL] React Query 심화

·2023년 12월 21일
1

TIL

목록 보기
61/85
post-thumbnail

Query Cancellation (쿼리 취소)


다운로드 UI가 있을 때, 또는 UX를 저해시키는 불필요한 네트워크 요청을 제거하기 위해 사용된다.
대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을줄일 수 있다.
queryFn 의 매개변수로 Abort Signal을 받을 수 있고, 이를 이용해서 Query 취소를 가능하게 한다.

QueryFunctionContext

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) => {}}

페이지 unmount 시 Query 취소

API 요청 시 기본 설정은 컴포넌트가 언마운트 되어도 네트워크 요청은 중단되지 않는다.
GET 요청 시 abort signal이 옵션으로 들어간 경우만 언마운트 시 자동으로 네트워크 취소가 된다.

import axios from 'axios'

const query = useQuery({
  queryKey: ['todos'],
  queryFn: ({ signal }) =>
    axios.get('/todos', {
      // Pass the signal to `axios`
      signal,
    }),
})

수동으로 Query 취소

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 된다.)

Optimistic Updates (낙관적 업데이트)

서버 요청이 정상적으로 잘 될거란 가정 하에 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

페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출(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,
  })
}

Paginated / Lagged Querys

다른 페이지 클릭 시 매번 Loading UI를 보여주기 보다는 기존 UI 를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용할 수 있다.
useQuery의 옵션 중 keepPreviousDatatrue로 바꾸면 이전 캐시데이터를 기반으로 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 을 스킵 가능. 이전 데이터 유지하라!
  });
profile
느리더라도 조금씩, 꾸준히

0개의 댓글