React Query2

한상욱·2024년 4월 1일

React Query

목록 보기
2/2

Parallel Queries

병렬적으로 쿼리 실행하는 방법을 알아봅시다.

Manual Parallel Queries

병렬적으로 불러야 하는 쿼리의 수가 변하지 않는다면 그냥 밑에다 쭉 풀어써라

const vendorQuery = useQuery({ queryKey: 'vendor', queryFn: getVendor });
const managerQuery = useQuery({ queryKey: 'manager', queryFn: getManager })

근데 이거 Suspense 모드에서는 안 먹힌다. 첫번째 쿼리 실행되고 나머지 쿼리는 실행 중지 상태로 변경되기 때문이다. 이때는 useSuspenseQueries를 사용해 전체를 묶거나 useSuspenseQuery를 쿼리 각각에 씌운다.

Dynamic Parallel Queries with useQuries

병렬로 실행해야 하는 쿼리가 렌더링 상황마다 달라지면 useQueries를 써라. 동적으로 쿼리를 병렬로 실행해준다.

const Vendor = ({ idList }: VendorProps) => {
  const userQueries = useQueries({
    queries: idList.map((vendor) => {
      return {
        queryKey: ['vendor', vendor.id],
        queryFn: () => getVendor(vendor.id),
      }
    }),
  })
}

useQuery dependent Query

특정 쿼리들에 영향을 받아 다음 쿼리 실행 여부를 결정하는 쿼리이다.

const { data: vendorId} = useQuery({ 
  queryKey: ['vendor', 1], 
  queryFn: getVendorByVendorId
});

const { data: manager } = useQuery({ 
  queryKey: ['manager', vendorId ], 
  queryFn: getManager,
  enabled: vendorId !== 0,
})

manager는 다음과 같은 과정을 거친다.

// manager 초기 단계
status: 'pending'
isPending: true
fetchStatus: 'idle'

// vendorId가 0이 아니고 enabled로 상태 변경
status: 'pending'
isPending: true
fetchStatus: 'fetching'

// 성공
status: 'success'
isPending: false
fetchStatus: 'idle'

useQueries dependent Query

useQueries도 의존성을 부여할 수 있다.

const { data: vendorIdList } = useQuery({
  queryKey: ['vendorIdList'],
  queryFn: getVendorList,
  select: (vendors) => vendors.map({ id } => id)
});

const managers = useQueries({
  queries: vendorIdList
    ? vendorIdList.map((id) => {
        return {
          queryKey: ['manager', id],
          queryFn: () => getManagerByVendor(id),
        }
      })
    : [],
})

note about dependant Query Performance

요청이 waterfall로 일어나서 퍼포먼스가 떨어진다. 직렬로 수행하면 병렬보다 시간이 배로는 걸린다. 이거 해야될 바에는 차라리 backend api 구조를 바꿔서 쿼리를 병렬로 호출하는 방안을 생각해라.

위에서는 getVendorByVendorIdgetManagerByVendor 대신 getManagerByVendorId가 더 나은 방법이다.

Background Fetching Indicators

쿼리가 데이터를 refetching 해오는 경우의 상태값이 필요한 경우가 있을 수 있다. 이때 isFetching을 사용한다.

const Todos = () => {
  const { data, status, isFetching } = useQuery({
    queryKey: ['vendor'],
    queryFn: getVendor
  });
  
  return match([status, isFetching, data])
    .with(['pending', P._, P._], () => <div>Loading</div>)
    .with(['error', P._, P._], () => <div>error</div>)
    .with([P._, true, P._], () => <div>Refreshing</div>)
    ...
}

Display Global Background Fetching Loading State

가끔 앱 내부에서 쿼리가 로딩 상태인지 궁금한 경우가 있다. 이때는 useIsFetching을 쓰면 된다.

const GlobalLoadingIndicator = () => {
  const isFetching = useIsFetching();
  
  return isFetching ? <div>Loading</div> : <></>
}

Window Focus Refetching

특정 탭을 보다가 다른 탭을 방문한 이후 다시 해당 탭으로 돌아 왔을 때 데이터가 stale하다면 Tanstack Query는 데이터를 갱신한다. refetchWindowFocus 옵션으로 제어하면 된다.

// 전역
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false, // default는 true
    }
  }
}
                                    
const App = () => 
  <QueryClientProvider client={queryClient}>
    ...
  </QueryClientProvider>

// 개별
useQuery({
  queryKey: ['vendor'],
  queryFn: getVendor,
  refetchOnWindowFocus: false,
});

Disabling/Pausing Queries

enabled 속성을 가지고 쿼리 동작을 제어할 수 있다.

enabled=false인 경우, 쿼리는 아래와 같이 동작한다.

  • 쿼리가 캐시 데이터를 가지면 쿼리는 status === 'success' 상태이거나 status === 'success'이다.
  • 쿼리가 캐시 데이터를 가지지 못하면 status === 'pending' 상태이거나 fetchStatus === 'idle'이다.
  • mount 시점에 쿼리 동작 안함
  • 내부적으로 쿼리 refetch 자동으로 안해줌
  • invalidateQueriesrefetchQuries 무시
  • useQuery의 리턴 값으로 나온 refetch는 쿼리를 패치하기 위해 사용된다. 하지만 이것이 skiptoken과 같이 동작하지 않는다.
    • enabled=false를 쓰지 말고 skipToken을 사용하자.

Lazy Queries

조건에 따라 enabled를 on/off하게 쓸 수 있다.

const Vendor = () => {
  const [filter, setFilter] = useState('');
  
  const { data } = useQuery({
    queryKey: ['vendors', filter],
    queryFn: () => getVendors(filter),
    enabled: filter !== '',
  });
  
  return (
    <div>
      {
        data.map(item => <span key={item.id}>{item.name}</span>
      }
    </div>
  );
}

isLoading

Lazy query는 처음에 데이터가 없기 때문에 status === 'pending'상태이다. 기술적으로는 맞지만 우리가 어떤 데이터도 현재 패칭받지 못한 시점이기 때문에 status를 이용해 로딩 아이콘을 보여줄 수 없다는 것을 의미한다.

만약 disable query나 lazy query를 다룰다면 status 대신 isLoading을 이용해라. 이것은 isPending && isFetching과 같은 역할을 한다.

따라서 현재 쿼리가 처음에 데이터만 가져올때만 isLoading === true일 것이다.

Typesafe disabling of queries using skipToken

typescript랑 tanstack query랑 같이 쓰면 skipToken을 이용할 수 있다. 이거 쓰면 쿼리를 조건에 따라 제어할 수 있으며 type safe하게 쿼리를 다룰 수 있다.

useQuery에서 나온 refetchskipToken과 같이 사용할 수 없다. 이 경우 skipTokenenabled: false와 같게 동작한다.

import { skipToken } from '@tanstack/react-query`;

const Vendor = () => {
  const [filter, setFilter] = useState('');
  
  const { data } = useQuery({
    queryKey: ['vendors', filter],
    queryFn: filter !== '' () => getVendors(filter) : skipToken,
  });
  
  return (
    <div>
      {
        data.map(item => <span key={item.id}>{item.name}</span>
      }
    </div>
  );
}

Query retries

useQuery의 쿼리 요청이 실패하면 자동으로 3번 재시도하게 설정되었다.

retry에는 아래의 값을 넣을 수 있다.

retry = false // 재시도 안함
retry = 6 // 재시도 6번
retry = true // 무한 재시도
retry = (failureCount ,error) => ... // 실패 처리를 custom logic으로 다루기

SSR에서는 렌더링 성능을 위해 retry가 기본 0으로 지정된다.

const query = useQuery({
  queryKey: ['vendor', 1],
  queryFn: getVendor,
  retry: 10,
});

Retry Delay

Retry 주기를 설정한다. ms 단위로 설정해야 한다. 근데 30초 넘으면 안됨. 기본값은 1000ms이다. 보통은 query 스토어 생성할 때 retryDelay를 전역으로 설정한다.

그리고 retryDelay를 셋팅할 때 숫자가 아닌 함수로 설정하자. 재시도 하는 중인데 또 재시도 할 수 있기 때문이다.

import {
  QueryCache,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
    },
  },
})

function App() {
  return <QueryClientProvider client={queryClient}>...</QueryClientProvider>
}
profile
그냥 뛰는 사람

0개의 댓글