본문 바로가기

온라인 강의(유데미, 인프런 등)/React-Query(유데미)

커스텀 쿼리 훅(관심사 분리) & 로딩 / 에러 처리 중앙집중화

반응형

커스텀 쿼리 훅

 

큰 앱들에서는 각 데이터 유형에 커스텀 훅을 만드는 것은 매우 흔하다. 커스텀 쿼리 훅을 사용할 때의 이점은 다음과 같다.

  1. 다수의 컴포넌트에서 데이터에 엑세스해야 하는 경우 useQuery 호출을 다시 작성하지 않아도 된다.
  2. 다수의 useQuery 호출을 사용했다면 사용 중인 키를 헷갈릴 수 있다. 커스텀 훅을 사용해 호출한다면 키를 헷갈릴 위험이 없다.
  3. 사용하길 원하는 쿼리 함수를 혼동할 위험이 없다. 
  4. 추상화를 통해 관심사 분리를 할 수 있다.

예시 코드는 다음과 같다.

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

반응형