[React 심화] TanStack Query 3 - 심화

조아영·2025년 3월 20일

◼ Query Cancellation

다운로드 UI가 있거나 UX를 저해하는 불필요한 네트워크 요청을 제거할 때 사용.
대용량 fetching을 중간에 취소하거나, 사용하지 않는 컴포넌트에서 fetching이 진행 중이면 자동으로 취소해 불필요한 네트워크 비용을 절감함.

사용방법

QueryFunctionContext
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 취소
API 요청의 기본 설정은 컴포넌트가 unmount 되어도 네트워크 요청이 중단되지 않음.
GET 요청은 abort signal이 옵션으로 전달된 경우에만 unmount 시 자동으로 네트워크 요청이 취소됨.

import axios from 'axios'

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

수동으로 Query 취소
queryClient.cancelQueries 사용해 특정 쿼리 취소 가능.

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

사용시 주의사항

모든 GET 요청마다 Abort Signal 적용 필요 여부
불필요한 네트워크 요청을 최소화한다는 명분으로, 단순히 모든 GET 요청에 Abort Signal을 적용하는 것은 권장되지 않음.
동영상 다운로드처럼 대용량 fetching이 아닌 이상, 대부분의 GET 요청은 빠르게 완료되고 캐싱 처리되어 성능에 유의미한 영향을 주지 못함.
대용량 fetching이 있거나 Optimistic UI를 구현하는 경우처럼 필요한 상황에서만 적용하는 것을 권장.

◼ Optimistic Updates

서버 요청이 성공할 것을 가정하고 UI를 먼저 변경한 뒤, 서버 요청을 보내는 방식.
요청이 실패하면 UI를 원상복구(revert/rollback)해야 함. 즉각적인 사용자 피드백을 제공할 수 있어 UX 향상에 효과적.

◼ Prefetching

페이지 이동 전에 이동할 페이지의 쿼리를 백그라운드에서 미리 호출(prefetching).
캐시 데이터가 있는 상태에서 해당 페이지로 이동하면 로딩 없이 바로 UI를 볼 수 있음.

const prefetchTodos = async () => {
  // 이동할 페이지의 queryKey, queryFn과 동일하게 설정 필요
  await queryClient.prefetchQuery({
    queryKey: ['todos'],
    queryFn: fetchTodos,
  })
}

◼ Paginated / Lagged Queries

페이지 이동 시 매번 Loading UI를 표시하는 대신, 기존 UI를 유지하다가 서버에서 새로운 데이터를 받아온 뒤에 화면을 갱신하는 방식을 적용.
useQuery 옵션에서 keepPreviousData: true를 설정하면 이전 캐시 데이터를 기준으로 isLoading 여부를 판단하므로, 페이지 전환 시 UX를 더 부드럽게 구성할 수 있음.

◼ Infinite Queries

데이터 fetching이 일어날 때마다 기존 리스트 데이터에 fetching된 데이터를 추가하고 싶을 때 유용하게 사용할 수 있는 훅. 더보기 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 실행
  4. 반환된 nextPageParam을 훅 내부 메모리에 저장 (캐시 저장 아님)
  5. nextPageParamundefined가 아닐 경우 hasNextPagetrue로 상태 변경
  6. fetchNextPage 실행
  7. 내부 저장된 nextPageParamqueryFn의 매개변수로 전달

pages 와 pageParams 를 갖는 캐시 데이터

  • useQueryqueryFn의 반환값이 그대로 캐시 데이터로 등록됨.
  • useInfiniteQueryqueryFn의 반환값이 pages 배열 요소로 추가됨.
  • 매개변수로 전달받은 pageParampageParams 배열 요소로 추가됨.

useInfiniteQuery 사용 시 주의사항

  • 훅의 내부 동작 원리로 인해 예상보다 리렌더링이 자주 발생할 수 있음.
  • 연산량이 많은 코드가 있다면 useMemo와 같은 메모이제이션 적용을 고려해야 함.
  • 리렌더링이 발생하더라도 실제 브라우저 렌더링이 항상 발생하는 것은 아님. Virtual DOM 기반 diff 과정을 거친 뒤 변경 사항만 반영됨.

0개의 댓글