조금 규모가 있는 앱에서는 Fetching과 Error에 대한 관리를 중앙화해주고 Refetching 해주는 과정이 있으면 좋다.
특히, 규모가 있는 앱들은 각 데이터 유형에 따른 커스텀 훅을 만든다.
이번에는 React Query를 규모가 있는 앱에서 사용했을 때 어떻게 깔끔하게 사용되는지 확인해보자.
✅ Custom Hook은 다음과 같은 장점이 있다.
다수의 컴포넌트에서 데이터에 접근해야할 경우 useQeury 호출을 매번 각각 작성할 필요가 없어진다.
다수의 useQuery 호출을 사용한다면 사용 중인 쿼리키의 종류를 헷갈릴 수 있기 때문에 커스텀 훅을 사용하면 이런 문제점이 사라진다.
사용해야하는 쿼리 함수를 혼동할 위험이 없어진다. 커스텀 훅에 함수를 정리해놓으면 다수의 컴포넌트에서 굳이 하나씩 불러올 필요가 없다.
업데이트시 각각 컴포넌트를 업데이트할 필요없이 커스텀 훅만 업데이트하면 된다.
✅ useTreatment라는 커스텀훅에서 쿼리를 생성해보자.
쿼리키 역시 미리 작성된 쿼리키를 사용하여 오타로 인한 에러를 방지하자.
쿼이함수로는 데이터 요청을 실행하는 getTreatments 함수를 사용하자.
async function getTreatments(): Promise<Treatment[]> {
const { data } = await axiosInstance.get('/treatments');
return data;
}
export function useTreatments(): Treatment[] {
const { data } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
✚ 쿼리키 모음
export const queryKeys = { treatments: 'treatments', appointments: 'appointments', user: 'user', staff: 'staff', };
💡 Axios Instance
Axios Instance 역시 React Query에서 사용하기 위한 커스텀훅인 useTreatement처럼 Axios에서 요청마다 중복적으로 사용되는 baseUrl, headers, timeout 등을 한 곳에서 관리해주는 커스텀훅의 개념이라고 생각하면 된다.(참고)
✅ fallback
data는 쿼리함수가 resolve될 때까지는 정의되지 않음으로 별도의 처리가 필요하다.
데이터에 대한 fallback을 빈값으로 생성하고, data의 기본값으로 설정한다. 그러면 데이터가 완벽히 정의되어 출력되기 전까지는 fallback(대체)값인 빈 배열이 출력된다. 다시말해 잠시 값을 대체하는 것이다.
따라서, 잠시 빈 화면이 떴다가 데이터가 출력되는 것을 확인할 수 있다.
export function useTreatments(): Treatment[] {
const fallback = [];
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments);
return data;
}
지금까지는 각각의 컴포넌트마다 로딩중이라는 문자출력과 같이 조기반환을 해주었는데, 로딩에 대한 처리를 한번에 처리할 수 있다.
React Query 훅인 useIsFetching을 통해서 각 컴포넌트마다 개별 로딩 기능이 아닌 중앙화된 로딩 기능을 사용할 수 있다.
useIsFetching은 현재 fetching
중인 쿼리의 개수를 return하는 훅이기 때문이다.
따라서, 데이터를 가져오는 중에는 로딩 Spinner를 켜고, 가져오는 데이터가 없으면 끄면 될 것이다.
✅ Spinner 작동
앞서 말했듯이 useIsFetching은 패칭중인 쿼리의 개수를 가져옴으로 0이 아닌 그 이상의 값이 반환된다면 isFetching의 값은 true가 되고, Spinner UI가 작동할 것이다.
// Loading.tsx
import { useIsFetching } from 'react-query';
export Loading = (): ReactElement => {
const isFetching = useIsFetching();
const display = isFetching ? 'inherit' : 'none';
return <Spinner ... display={display} />
}
💡 useIsFetching과 queryClient.isFetching은 결과가 다르다.
useIsFetching
은 현재fetching
중인 쿼리의 개수를 리턴하는 훅이다.queryClient
의isFetching
또한 동일한 기능을 하는 함수다.
useIsFetching
은 내부적으로queryClient
의queryCache
들을Observing
하면서fetching
개수를 리턴하지만,queryClient.isFetching
은Observing
하지 않고 현재 캐싱 된 데이터의 fetching 개수를 리턴한다.export default function Test() { const queryClient = useQueryClient(); const isFetching = useIsFetching(); const isFetching2 = queryClient.isFetching(); const test = useUserData(); /* 0 0 0 1 1 1 */ console.log(isFetching, isFetching2); return <></>; }
결과적으로는 동일한 기능을 하지만, 위와 같이 결과가 다른 이유는
useIsFetching
의 내부에서는Observing
의 결과를useEffect
와useState
통해 값을 리턴하기 때문에,queryclient.isFetching
으로 사용하는 것보다 한 틱 더 늦게 값을 return 한다는 걸 알 수 있다. (출처)
이전에는 useQuery의 반환값으로 isError와 error를 받아서 오류처리를 해주었고, 각각의 컴포넌트마다 에러처리를 해주었다.
✅ 이번엔 Error처리 또한 중앙화해주자.
쿼리 함수가 에러를 발생시키면 onError callback이 실행되고 React Query가 callback에 여러 매개변수를 전달하기 때문에 모든 컴포넌트에 에러 처리가 가능하다.
즉, 각각 상황에 따른 다른 에러처리를 할 수 있는 것이다.
💡 에러처리에 대해 출력될 Error Box UI로 Chakra UI의 useToast 훅이 유용하므로 참고하자! (공식문서)
✅ useQuery에 onError callback 추가
export function useTreatments(): Treatment[] {
const toast = useCustomToast(); // toast UI에 대한 커스텀훅
const fallback = [];
const { data = fallback } = useQuery(queryKeys.treatments, getTreatments, {
onError: (error) => {
const title =
error instanceof Error // 만약 error가 JS Error 클래스의 인스턴스라면
? error.message
: 'error connnecting to the server';
toast({ title, status: 'error' });
},
});
return data;
}
✅ 서버를 끄고 네트워크 오류 발생시킬 경우
✅ onError 기능 뿌려주기
useTreatment에서 생성한 onError는 Treatment 페이지에 대한 오류처리이므로 지워주고, 이제 QueryClient에서 onError를 처리하여 모든 useQuery 호출에 적용시키자.
// queryClient.ts
const toast = createStandaloneToast({ theme });
const queryErrorHandler = (error: unknown): void => {
const title = error instanceof Error ? error.message : 'error connecting to server';
toast.closeAll();
toast({ title, status: 'error', variant: 'subtle', isClosable: true });
}
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
onError: queryErrorHandler,
},
},
});