커스텀 쿼리 훅
큰 앱들에서는 각 데이터 유형에 커스텀 훅을 만드는 것은 매우 흔하다. 커스텀 쿼리 훅을 사용할 때의 이점은 다음과 같다.
- 다수의 컴포넌트에서 데이터에 엑세스해야 하는 경우 useQuery 호출을 다시 작성하지 않아도 된다.
- 다수의 useQuery 호출을 사용했다면 사용 중인 키를 헷갈릴 수 있다. 커스텀 훅을 사용해 호출한다면 키를 헷갈릴 위험이 없다.
- 사용하길 원하는 쿼리 함수를 혼동할 위험이 없다.
- 추상화를 통해 관심사 분리를 할 수 있다.
예시 코드는 다음과 같다.
api 요청 코드, useQuery 구문을 커스텀 훅 파일에 넣어 관심사를 분리한다.
// useTreatments.ts
import { useQuery, useQueryClient } from 'react-query';
import type { Treatment } from '../../../../../shared/types';
import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
// for when we need a query function for useQuery
async function getTreatments(): Promise<Treatment[]> {
const { data } = await axiosInstance.get('/treatments');
return data;
}
export function useTreatments(): Treatment[] {
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
// treatments.tsx
import { Box, Heading, HStack } from '@chakra-ui/react';
import { ReactElement } from 'react';
import { useTreatments } from './hooks/useTreatments';
import { Treatment } from './Treatment';
export function Treatments(): ReactElement {
// replace with data from React Query
const treatments = useTreatments();
return (
<Box>
<Heading mt={10} textAlign="center">
Available Treatments
</Heading>
<HStack m={10} spacing={8} justify="center">
{treatments.map((treatmentData) => (
<Treatment key={treatmentData.id} treatmentData={treatmentData} />
))}
</HStack>
</Box>
);
}
폴 백 데이터
useQuery가 data를 가져오기 전까지는 data가 없으므로 map으로 뿌려줄 데이터가 없다는 오류가 뜬다. 이럴 때는 폴 백 데이터인 빈 배열을 설정하여 캐시가 빈 경우 잠시 잠깐 아무 것도 표시하지 않도록 설정해줄 수 있다.
이렇게 되면 화면에 잠깐 아무것도 안떴다가(빈 배열이므로 렌더링 할 게 없음) data가 받아와지면 data가 보인다.
// 컴포넌트
import { useQuery, useQueryClient } from 'react-query';
import type { Treatment } from '../../../../../shared/types';
import { axiosInstance } from '../../../axiosInstance';
import { queryKeys } from '../../../react-query/constants';
// import { useCustomToast } from '../../app/hooks/useCustomToast';
// for when we need a query function for useQuery
async function getTreatments(): Promise<Treatment[]> {
const { data } = await axiosInstance.get('/treatments');
return data;
}
export function useTreatments(): Treatment[] {
// 데이터에 대한 폴백값을 생성하여 확보
const fallback = []; // 서버에서 데이터를 받지 않고 캐시가 비어있는 경우 아무 것도 표시하지 않도록 설정
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments); // 아규먼트가 없으므로 함수만 전달 가능
return data;
}
로딩 인디케이터 중앙 집중화시키기(useIsFetching 훅)
각 컴포넌트마다 로딩 인디케이터를 두기보단 중앙화된 로딩 인디케이터가 사용되도록 하는 방법도 있다. 이 때는 useIsFetching 훅을 사용한다.
작은 앱에서는 isFetching을 사용했었다. 더 큰 앱인 경우 어떤 쿼리가 데이터를 가져올 경우 로딩 스피너를 가져오도록 로딩 스피너를 주앙화 시킬 수 있다.
useIsFetching은 현재 데이터를 가져오기 중인 쿼리가 있는지를 알려준다. 이것을 사용함으로써 우리는 각각의 커스텀 훅에 대해 isFetching 훅을 사용할 필요가 없다. 대신 useIsFetching 훅을 로딩 컴포넌트에 사용할 수 있고 이 useIsFetching 값은 스피너의 표시 여부를 우리에게 가르쳐준다.
useIsFetching
useIsFetching은 현재 가져오기 상태인 쿼리 호출의 수를 나타내는 정수값을 반환한다.
// 로딩 컴포넌트
import { Spinner, Text } from '@chakra-ui/react';
import { ReactElement } from 'react';
import { useIsFetching } from 'react-query';
export function Loading(): ReactElement {
// will use React Query `useIsFetching` to determine whether or not to display
const isFetching = useIsFetching();
const display = isFetching ? 'inherit' : 'none';
return (
<Spinner
thickness="4px"
speed="0.65s"
emptyColor="olive.200"
color="olive.800"
role="status"
position="fixed"
zIndex="9999"
top="50%"
left="50%"
transform="translate(-50%, -50%)"
display={display}
>
<Text display="none">Loading...</Text>
</Spinner>
);
}
// app.tsx
import { ChakraProvider } from '@chakra-ui/react';
import { ReactElement } from 'react';
import { QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools';
import { queryClient } from '../../react-query/queryClient';
import { theme } from '../../theme';
import { Loading } from './Loading';
import { Navbar } from './Navbar';
import { Routes } from './Routes';
export function App(): ReactElement {
return (
<ChakraProvider theme={theme}>
{/* ChakraProvider 내부에 세팅하는 이유 = 오류 토스트 메시지 표시할 때 차크라 세팅이 적용되도록 */}
<QueryClientProvider client={queryClient}>
<Navbar />
<Loading />
<Routes />
<ReactQueryDevtools />
</QueryClientProvider>
</ChakraProvider>
);
}
에러 핸들링 중앙화
쿼리 함수가 에러를 발생시키면 onError 콜백이 실행되는데 리액트 쿼리가 콜백에 에러 파라미터를 전달하기 때문에 우리가 원하는 대로 처리할 수 있다. (참고로 Chakra UI의 useToast 훅을 사용하면 아주 쉽게 에러 토스트를 처리할 수 있다.)
이제 에러 핸들링을 중앙화 해보자. 모든 useQuery 호출에 오류 핸들링 방식을 적용해서 각 호출에 따로 지정하지 않게 만들어야 한다.
리액트 쿼리에서 useError 훅을 제공하지 않는 이유는 반환값이 정수 이상이 되어야 하기 때문이다. 사용자에게 오류를 표시하려면 각 오류에 대한 문자열이 필요한데 각기 다른 문자열을 가진 오류가 시시각각 팝업하도록 구현하기란 쉽지 않을 것이다. 따라서 집중식 훅 대신 onError 기본 핸들러를 만들어야 한다.
일반적으로 QueryClient는 쿼리나 변이에 대해 기본값을 가질 수 있다. QueryClient는 options 객체를 가질 수 있고, 두 가지 프로퍼티를 가질 수 있다.
예시 코드는 다음과 같다.
import { createStandaloneToast } from '@chakra-ui/react';
import { QueryClient } from 'react-query';
import { theme } from '../theme';
const toast = createStandaloneToast({ theme });
function queryErrorHandler(error: unknown): void {
// error is type unknown because in js, anything can be an error (e.g. throw(5))
const title =
error instanceof Error ? error.message : 'error connecting to server';
// prevent duplicate toasts
toast.closeAll();
toast({ title, status: 'error', variant: 'subtle', isClosable: true });
}
// 오류 핸들러의 기본값을 사용하도록 옵션을 설정하여 중앙집중식 오류 핸들링
// useQuery마다 오류 핸들러를 추가할 필요가 없게됨
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
},
},
});
사용처의 코드를 보자. 이제 useQuery에 에러 처리를 하지 않아도 된다.
// 컴포넌트
export function useTreatments(): Treatment[] {
const fallback = [];
// queryClient의 디폴트 옵션에서 queries 옵션으로 onError속성에 에러 핸들러를 추가해줬기 때문에
// 에러 처리 하지 않아도 됨
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
onError의 대안책으로 에러 바운더리를 사용하는 방안이 있다.
리액트 쿼리의 에러를 따로 처리하고 싶지 않다면 useQuery의 useErrorBoundary 옵션을 사용하면 된다. 각 쿼리 또는 변이에 옵션으로 추가하거나 onError 핸들러처럼 기본값으로 적용할 수 있다. 이 옵션을 true로 하면 리액트 쿼리가 내에서 에러를 처리하는 것 대신 가장 가까운 에러 경계로 에러를 전파한다.
기본 코드 깃허브)
https://github.com/bonnie/udemy-REACT-QUERY/tree/main/base-lazy-days/client/src
'온라인 강의(유데미, 인프런 등) > React-Query(유데미)' 카테고리의 다른 글
쿼리 특성 2 : select 옵션을 통한 데이터 filtering과 refetching & polling (0) | 2023.09.01 |
---|---|
쿼리 특성 1 : Pre-fetching과 페이지네이션 (0) | 2023.09.01 |
useInfiniteQuery (0) | 2023.08.31 |
쿼리 키 & 페이지네이션 & 데이터 pre-fetching & isLoading / isFetching & Mutation (0) | 2023.08.30 |
React Query 개발자 도구 & staleTime vs cacheTime (0) | 2023.08.30 |