본문 바로가기

공부/상태 관리

TanStack Query 알아보기 3 (Query Cancellation, Optimistic Updates, Prefetching, Paginated, Infinite Queries)

반응형

 

 

Query Cancellation 📖

불필요한 네트워크 요청을 제거하기 위해 사용합니다.

cancel

 

대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을 줄일 수 있습니다.

 

queryFn의 매개변수로 Abort`Signal`을 받을 수 있고 이를 활용해 Query 취소가 가능합니다.

 

사용 방법

queryFn은 매개변수로 `QueryFunctionContext`객체를 받습니다.

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 취소

import axios from 'axios'

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

 

수동으로 Query 취소

const query = useQuery({
  queryKey: ['todos'],
  queryFn: async ({ signal }) => {
    const resp = await fetch('/todos', { signal })
    return resp.json()
  },
})

const queryClient = useQueryClient()

return (
  <button
    onClick={(e) => {
      e.preventDefault()
      queryClient.cancelQueries({ queryKey: ['todos'] })
    }}
  >
    Cancel
  </button>
)

 

Optimistic Updates 📖

서버에 요청이 정상임을 가정하고 더 나은 UX를 제공하는 방법이고 낙관적 업데이트라고 합니다.

 

Optimistic Updates

 

사용 방법

const addMutation = useMutation({
  // 2번
  mutationFn: addTodo,
  //onSuccess: () => {
  //  queryClient.invalidateQueries(['todos']);
  //},
  
  // 1번
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] });
    
    const prevTodos = queryClient.getQueryData(['todos']);
    
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);
    
    return { prevTodos };
  },
  
  // 3번
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.prevTodos);
  },
  
  // 4번
  onSettled: () => {
  	queryClient.invalidateQueries({ queryKey: ['todos'] });
  }
})

 

addMutation.mutate({content})가 호출되는 순간, `addTodo`가 호출될 것이라고 생각하겠지만 아닙니다!

 

실행 순서

1. onMutate

2. mutationFn: addTodo

3. onError

4. onSettled

 

위 순서대로 실행이 됩니다.

 

동작 과정 

 

1. `onMutate`가 처음 실행되고 `todos`라는 `queryKey`로 데이터를 가져오고 있는 중이라면 사이드 이펙트 문제가 발생할 수 있기 때문에`취소`를 합니다.

await queryClient.cancelQueries({ queryKey: ['todos'] });

 

2. 현재 가지고 있는 `todos`데이터를 백업 합니다.

const prevTodos = queryClient.getQueryData(['todos']);

 

3. `setQueryData`를 이용해 현재 가지고 있는 `todos`데이터를 가져와 `newTodo`와 합쳐서 query데이터를 갱신하고 prevTodos를 리턴합니다.

queryClient.setQueryData(['todos'], (old) => [...old, newTodo]);

return { prevTodos }

 

위 과정은 서버에 갔다 온 로직이 아니고 클라이언트에서 todoList를 갱신하는 로직입니다.

 

4. 리턴 받은 `prevTodos`는 `onError`의 context로 들어가게 됩니다. 

onError: (err, newTodo, context) => {
  queryClient.setQueryData(['todos'], context.prevTodos);
}

 

5. `addTodo`가 성공했다면 `invalidateQueries`를 통해서 데이터를 최신 상태로 유지합니다.

onSettled: () => {
 queryClient.invalidateQueries({ queryKey: ['todos'] });
}

 

Prefetching 📖

페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출합니다. 캐시 데이터가 있는 상태로 해당 페이지로 이동 시 로딩 없이 바로 UI를 볼 수 있습니다.

const prefetchTodos = async () => {
  // prefetch 할 queryKey와 queryFn 은 이동할 페이지의 쿼리와 동일해야 적절합니다.
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

 

Paginated / Lagged Queries 📖

다른 페이지를 클릭 시 매번 Loading UI를 보여주기보다는 기존 UI를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식입니다.

 

`keepPreviousData`속성을 true로 하게 되면 원래 가지고 있는 데이터를 유지하다가 새로운 데이터를 가져오면 바꿔주겠다는 의미입니다.

const { data: moives, inPending } = useQuery({
  queryKey: ['movies', page],
  queryFn: fetchMovieData,
  keepPreviousData: true,
})

 

Infinite Queries 📖

Data Fetching이 일어날 때마다 기존 리스트 데이터에 fetched data를 추가하고자 할 때 유용하게 사용할 수 있는 훅입니다.

더 보기 UI 또는 무한스크롤 UI에 사용하기 적합합니다.

무한스크롤

 

사용 방법

const fetchProjects = async ({ pageParam = 0 }) => {
    const res = await fetch('/api/projects?cursor=' + pageParam)
    return res.json()
  }

  const {
    data,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    status,
  } = useInfiniteQuery({
    queryKey: ['projects'],
    queryFn: fetchProjects,
    getNextPageParam: (lastPage, pages) => lastPage.nextCursor,
  })

 

실행 순서

동작

 

1. queryFn 실행

2. 캐시 데이터 등록 { pages, pageParams }

3. getNextPageParam 실행 (리턴된 NextPageParam은 훅 내부 메모리에 저장. 캐시 x)

4. NextPageParam이 undefined가 아니라면 hasNextPage `true`로 변경

5. fetchNextPage 실행

6. queryFn 실행 (이때 내부적으로 저장되어 있던 NextPageParam을 queryFn의 매개변수로 넘겨줌)

 

Observer

`react-intersection-observer`를 설치합니다.

yarn add react-intersection-observer

 

`threshold`속성을 사용해 observer가 보이게 되면 다음 페이지를 요청합니다.

const { ref } = userInView({
  threshold: 1,
  onChange: (inView) => {
    if(!inView || !hasNextPage || isFetchingNextPage) return;
    fetchNextPage();
  }
})

 

 

이전 게시물

 

TanStack Query 알아보기 2 (동작 원리)

SWR 전략 📖stale을 사전에 검색해 보면 `신선하지 않은``오래된`이라는 뜻을 가지고 있습니다. 말 그대로 `SWR(stale while revalidate)`은 최신 데이터가 도착하기 전까지 기존 캐시 데이터를 사용하는

mingos-habitat.tistory.com


출처 🏷️

https://www.heropy.dev/p/ydKoQO

반응형