React Query - Custom Hooks

박정호·2023년 1월 11일
1

React Query

목록 보기
6/14
post-thumbnail

🚀 Start

조금 규모가 있는 앱에서는 Fetching과 Error에 대한 관리를 중앙화해주고 Refetching 해주는 과정이 있으면 좋다.

특히, 규모가 있는 앱들은 각 데이터 유형에 따른 커스텀 훅을 만든다.

이번에는 React Query를 규모가 있는 앱에서 사용했을 때 어떻게 깔끔하게 사용되는지 확인해보자.



🪝 Custom Hooks

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;
}


👉 useIsFetching

지금까지는 각각의 컴포넌트마다 로딩중이라는 문자출력과 같이 조기반환을 해주었는데, 로딩에 대한 처리를 한번에 처리할 수 있다.

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 중인 쿼리의 개수를 리턴하는 훅이다. queryClientisFetching 또한 동일한 기능을 하는 함수다.

useIsFetching은 내부적으로 queryClientqueryCache들을 Observing 하면서 fetching 개수를 리턴하지만, queryClient.isFetchingObserving 하지 않고 현재 캐싱 된 데이터의 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의 결과를 useEffectuseState 통해 값을 리턴하기 때문에, queryclient.isFetching으로 사용하는 것보다 한 틱 더 늦게 값을 return 한다는 걸 알 수 있다. (출처)



👉 useQuery + onError 핸들러

이전에는 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;
}

서버를 끄고 네트워크 오류 발생시킬 경우

  • React Query의 기본 설정에 따라 작업을 중단하고 에러를 표시하기까지 총 3번의 시도 확인!


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,
    },
  },
});
profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글