React

[리액트] Tanstack Query 쿼리취소, 페이지네이션, 무한스크롤

ejunyang 2024. 6. 28. 23:09

1. Query Cancellation

  • 대용량 fetching을 중간에 취소하거나 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소시켜 불필요한 네트워크 비용을 줄일 수 있다
  • queryFn 의 매개변수로 Abort Signal 을 받을 수 있고, 이를 이용해서 Query 취소 가능

 

 

✏️ 사용방법

  • QueryFunctionContext
  • queryFn 은 매개변수로 QueryFunctionContext 이란 객체를 받는다

원래 데이터를 서버로부터 가지고 올 때 받는 매개변수 받는 곳을 비워놓았었는데, 비워져있는게 아닌 Tastack query 에서는 이미 queryFnContext가 들어가 있다. 두번째 인자에 signal 을 넣어주면 GET 요청 시 abort signal 이 옵션으로 들어간 경우에만 unmount 시 자동으로 네트워크 취소가 된다.

export const getTodos = async (queryFnContext) => {
  const { queryKey, pageParam, signal, meta } = queryFnContext;
  const response = await axios.get("http://localhost:5000/todos", { signal });
  return response.data;
};

useQuery({
  queryKey: ["todos"],
  queryFn: getTodos,
})
// example: <div onClick={(event) => {}}

 

queryFnContext 안에는 몇가지 요소가 있는데 아래와 같다.

1. queryKey: 배열형태의 쿼리키
2. pageParam: useInfiniteQuery 사용 시 getNextPageParam 실행 시 적용
3. signal: AbortSignal 을 의미 (네트워크 요청을 중간에 중단시킬 수 있는 장치)
4. meta: query에 대한 정보를 추가적으로 메모를 남길 수 있는 string 필드

 

 

🚨 주의

  • 불필요한 네트워크 요청을 최소화 한다는 명분으로 단순하게 모든 GET 요청마다 Abort Signal 을 심는 것은 작업부하를 올리고 바람직하지 않다.
  • 동영상 다운로드 같은 대용량 fetching이 아닌 이상 대부분의 GET 요청은 빠르게 완료 및 캐싱처리되어 성능에 유의미한 영향을 끼치지 못한다.
  • 대용량 fetching 이 있는 경우 또는 Optimistic UI 를 구현할 때처럼 필요한 경우에만 적용하는 것을 권장

 

 

2. Optimistic Updates

  • 낙관적 업데이트, 네트워크 요청이 끝나기 전에 미리 UI 변경
  • 서버 요청이 정상적으로 잘 될거란 가정하에 UI 변경을 먼저하고, 서버 요청 하는 방식. 혹시라도 서버 요청이 실패하는 경우, UI 를 원상복구(revert / roll back)
  • 좋아요 기능 특징

 

✏️ 사용방법

실행 순서는 아래 주석과 같다.

1. onMutate

  • await queryClient.cancelQueries({ queryKey: ['todos'] }) : 쿼리를 불러오는 중이라면 사이드 이펙트를 막기위해 쿼리 취소를 해주어야한다.
  • const previousTodos = queryClient.getQueryData(['todos']) : 현재 가지고 있는 데이터를 저장해둔다.
  • queryClient.setQueryData(['todos'], (old) => [...old, newTodo]) : 추가된 데이터 UI 갱신
  • return { previousTodos } : previousTodos는 onError에 context 로 들어간다.

2. mutationFn

3. onError

  • return { previousTodos } : previousTodos는 onError에 context 로 들어간다.
  • queryClient.setQueryData(['todos'], context.previousTodos) : 쿼리가 에러가 났을 때 기존에 저장된 데이터로 원복처리

4. onSettled

  • queryClient.invalidateQueries({ queryKey: ['todos'] }) : 쿼리 최신화
const queryClient = useQueryClient()

useMutation({
  // 2. 실행
  mutationFn: updateTodo,
  
  // 1. 실행
  onMutate: async (newTodo) => {
    await queryClient.cancelQueries({ queryKey: ['todos'] })
    const previousTodos = queryClient.getQueryData(['todos'])
    queryClient.setQueryData(['todos'], (old) => [...old, newTodo])
    return { previousTodos }
  },
  
  // 3. 실행
  onError: (err, newTodo, context) => {
    queryClient.setQueryData(['todos'], context.previousTodos)
  },
  // 4. 실행
  onSettled: () => {
    queryClient.invalidateQueries({ queryKey: ['todos'] })
  },
})

 

 

3. 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,
  })
}

 

 

4. Paginated / Lagged Queries

  • 기존 UI를 유지하다가 서버로부터 새로운 데이터를 받아왔을 때 바꾸는 방식을 적용
  • useQuery의 옵션 중 keepPreviousDatatrue 로 바꾸면 데이터를 가져오기 전까진 기존 데이터를 가져와서 사용한다.

 

✏️ 사용방법

export default function MoviePagination () {
	const [page, setPage] = useState(1);
	const { data: movies, isPending } = useQuery ({
		queryKey: ["movies", page], 
		queryFn: fetchMovieData,
		select: ({ total_pages, results }) => ({
			total_pages, results,
		}),

		keepPreviousData: true,
});

 

🚨 주의

로딩 중임을 사용자에게 명시적으로 보여줘야할 때, 쿼리를 가져오는데 5초 이상이 걸린다면 사용자는 5초동안 갱신되지 않는 상태의 UI를 보게된다. 그럴때에는 로딩 중임을 보여주는 것이 좋다. 여러 상황에 맞게 처리해주는 것이 좋다.

 

 

5. Infinite Queries

  • Data Fetching 이 일어날 때 마다 기존 리스트 데이터에 Fetched Data 를 추가하고자 할 때 유용하게 사용할 수 있는 hook
  • 더보기 UI 또는 무한스크롤 UI 에 사용하기에 적합

 

실행 순서는 아래와 같다.

queryFn 실행 
→ 캐시 데이터 등록 { pages, pageParam }  
→ getNextPageParam 실행 (리턴된 NextPageParam는 훅 내부 메모리에 저장. 캐시에 저장X) 
→ (NextPageParam 이 undefined이 아니면) hasNextPage true로 상태변경 
→ fetchNextPage 실행 
→ queryFn 실행 (이 때 내부적으로 저장되어 있던 NextPageParam을 queryFn 의 매개변수로 넘겨줌)

 

✏️ 사용방법

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,
  })