병렬적으로 쿼리 실행하는 방법을 알아봅시다.
병렬적으로 불러야 하는 쿼리의 수가 변하지 않는다면 그냥 밑에다 쭉 풀어써라
const vendorQuery = useQuery({ queryKey: 'vendor', queryFn: getVendor });
const managerQuery = useQuery({ queryKey: 'manager', queryFn: getManager })
근데 이거 Suspense 모드에서는 안 먹힌다. 첫번째 쿼리 실행되고 나머지 쿼리는 실행 중지 상태로 변경되기 때문이다. 이때는 useSuspenseQueries를 사용해 전체를 묶거나 useSuspenseQuery를 쿼리 각각에 씌운다.
useQuries병렬로 실행해야 하는 쿼리가 렌더링 상황마다 달라지면 useQueries를 써라. 동적으로 쿼리를 병렬로 실행해준다.
const Vendor = ({ idList }: VendorProps) => {
const userQueries = useQueries({
queries: idList.map((vendor) => {
return {
queryKey: ['vendor', vendor.id],
queryFn: () => getVendor(vendor.id),
}
}),
})
}
특정 쿼리들에 영향을 받아 다음 쿼리 실행 여부를 결정하는 쿼리이다.
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도 의존성을 부여할 수 있다.
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),
}
})
: [],
})
요청이 waterfall로 일어나서 퍼포먼스가 떨어진다. 직렬로 수행하면 병렬보다 시간이 배로는 걸린다. 이거 해야될 바에는 차라리 backend api 구조를 바꿔서 쿼리를 병렬로 호출하는 방안을 생각해라.
위에서는 getVendorByVendorId와 getManagerByVendor 대신 getManagerByVendorId가 더 나은 방법이다.
쿼리가 데이터를 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>)
...
}
가끔 앱 내부에서 쿼리가 로딩 상태인지 궁금한 경우가 있다. 이때는 useIsFetching을 쓰면 된다.
const GlobalLoadingIndicator = () => {
const isFetching = useIsFetching();
return isFetching ? <div>Loading</div> : <></>
}
특정 탭을 보다가 다른 탭을 방문한 이후 다시 해당 탭으로 돌아 왔을 때 데이터가 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,
});
enabled 속성을 가지고 쿼리 동작을 제어할 수 있다.
enabled=false인 경우, 쿼리는 아래와 같이 동작한다.
status === 'success' 상태이거나 status === 'success'이다.status === 'pending' 상태이거나 fetchStatus === 'idle'이다.refetch 자동으로 안해줌invalidateQueries와 refetchQuries 무시useQuery의 리턴 값으로 나온 refetch는 쿼리를 패치하기 위해 사용된다. 하지만 이것이 skiptoken과 같이 동작하지 않는다.enabled=false를 쓰지 말고 skipToken을 사용하자.조건에 따라 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>
);
}
Lazy query는 처음에 데이터가 없기 때문에 status === 'pending'상태이다. 기술적으로는 맞지만 우리가 어떤 데이터도 현재 패칭받지 못한 시점이기 때문에 status를 이용해 로딩 아이콘을 보여줄 수 없다는 것을 의미한다.
만약 disable query나 lazy query를 다룰다면 status 대신 isLoading을 이용해라. 이것은 isPending && isFetching과 같은 역할을 한다.
따라서 현재 쿼리가 처음에 데이터만 가져올때만 isLoading === true일 것이다.
skipTokentypescript랑 tanstack query랑 같이 쓰면 skipToken을 이용할 수 있다. 이거 쓰면 쿼리를 조건에 따라 제어할 수 있으며 type safe하게 쿼리를 다룰 수 있다.
useQuery에서 나온refetch는skipToken과 같이 사용할 수 없다. 이 경우skipToken은enabled: 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>
);
}
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 주기를 설정한다. 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>
}