[🚨응급실 알리미] React-query에서 error 처리

    프로젝트를 진행하면서 일관된 에러 처리에 대한 중요성을 느꼈습니다.

    에러가 발생하면 사용자에게 적절하게 알리고, 일관된 방식으로 대응하는 것은 사용자의 경험을 향상 시키는데 큰 역할을 합니다.  

     

    [응급실 알리미] 서비스의 발생할 수 있는 에러 상황과 대응 방법

    응급실 알리미는 로그인 없이 응급실 정보를 제공하며, 정보는 일방향으로만 제공됩니다.
    따라서 일반적으로 권한이 없는 유저가 접근했을때, 데이터 변경 HTTP 메서드에 따른 에러 처리를 하지 않았습니다.

    1. get이 실패한 상황(api 요청 에러)

    • 사용자
      • 화면에 각 API에 따른 유의미한 메시지와 함께 toast 알림으로 표시한다.
      • 새로 고침 버튼을 넣어 새로 고침을 유도하여 다시 API를 요청한다.
    • 개발자
      • error 로그를 남긴다

    2. 404가 발생하는 상황 

    ( 잘못된 URL 정규식을 방문했을 경우, 유효한 URL 정규식을 방문했지만 잘못된 query string을 입력한 경우)

    • 사용자
      • <NotFoundPage /> 페이지로 이동한다.
      • 메인으로 가는 버튼과 뒤로 가는 버튼을 제공한다.

     

     

    React Query의 Error 대응 방법

    TkDodo의 블로그에 따르면 React Query에서 error를 다루는 세 가지 주요 방법은 다음과 같습니다.

    1. useQuery에서 반환된 error 속성
    2. onError 콜백 (query자체 또는 global QueryCache/ MutationCache)
    3. Error bounderies 사용

     

    현재 v4버전인 React Query에선 useQuery에서 오류가 발생했을 때  호출되는 onError 콜백 함수를 제공하고 있습니다. 하지만 공식 문서에도 언급되어 있듯이 v5버전에는 사라질 함수라고 명시되어 있습니다.

     

    왜 그럴까요?


    두 개의 컴포넌트가 onError콜백에서 에러를 알려주는 알림이 제공하는 useQuery 커스텀 훅을 호출한다고 가정했을때, 애플리케이션에서 에러가 발생한다면, 1개의 에러 알림이 아닌 2개의 에러 알림을 받게됩니다. 여기서 useQuery를 호출하는 컴포넌트를 Obsever라고 하는데, 만약 여러 개의 Obsever에 에러가 발생한다면 사용자는 동일한 API 호출이 실패했음에도 불구하고 여러 개의 에러 알림을 받게 됩니다.

    그럼 어떻게 해결하면 좋을까요?

    메인 테이너인 Dkdodo은 queryClient를 세팅할 때 전역 캐시 단계에서 에러 콜백을 사용하라고 조언하고 있습니다. 쿼리당 한 번의 onError콜백을 보장하기 때문입니다.

    그래서 응급실 알리미는?

    각 useQuery에 meta필드를 사용해 유의미한 에러 메시지를 저장하고 queryClient에서 error발생 시 유의미한 에러 메시지를 가진 토스트 알림을 사용자에게 제공합니다.

     

    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import { RouterProvider } from 'react-router-dom';
    import toast from 'react-hot-toast';
    import {
      QueryClient,
      QueryClientProvider,
      QueryCache,
    } from '@tanstack/react-query';
    import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
    import { RecoilRoot } from 'recoil';
    import { router } from './routers';
    
    const queryClient = new QueryClient({
      defaultOptions: {
        queries: {
          refetchOnWindowFocus: false,
          refetchOnMount: false,
          refetchOnReconnect: false,
        },
      },
      queryCache: new QueryCache({
        onError: (error, query) => {
          if (query.meta.errorMessage) toast(() => query.meta.errorMessage);
        },
      }),
    });
    
    ReactDOM.createRoot(document.getElementById('root')).render(
      <React.StrictMode>
        <RecoilRoot>
          <QueryClientProvider client={queryClient}>
            <RouterProvider router={router} />
            <ReactQueryDevtools initialIsOpen={true} />
          </QueryClientProvider>
        </RecoilRoot>
      </React.StrictMode>,
    );

     

    import { useSetRecoilState } from 'recoil';
    import { useQuery } from '@tanstack/react-query';
    import { getErList } from '@services';
    import { ersListState } from '@stores';
    import { ErrorMessage } from '@components';
    import { useCallback } from 'react';
    
    // 응급실 전체 목록 정보 가져오기
    function useFetchErList() {
      const setErsListState = useSetRecoilState(ersListState);
    
      const query = useQuery({
        queryKey: ['erList'],
        queryFn: getErList,
        retry: 3,
        select:useCallback(
          (data) => setErsListState(data),
          []
        ),
        meta: {
    	// ErrorMessage 컴포넌트는 제공할 디자인에 맞게 공통 컴포넌트 생성함
          errorMessage: (
            <ErrorMessage
    		// 각 쿼리별로 유의미한 메시지를 전달함
              content="[실패] 응급실 목록 데이터 가져오기"
              refreshButton
            />
          ),
        },
      });
    
      return query;
    }
    
    export default useFetchErList;

     

    또한 useQuery를 observer하고 있는 컴포넌트에 susepnse와 error boundary를 사용하여 로딩중일땐 스피너를 제공하고, 에러 발생 시 컴포넌트에 에러 메시지를 제공하고 있습니다.

    해당 쿼리 옵션에  suspense: true  설정해두고 useQuery를 호출한 부모 컴포넌트에서 ErrorBoundary와 Suspense를 사용해줍니다.
    아래 코드에선 HpSriIllData 컴포넌트에서 useQuery를 호출해줬습니다.

    function HpSriIllContent() {
      return (
        <StyledHpSriIllContent>
          <Title>중증응급질환 수술 여부</Title>
          <ErrorBoundary
            fallback={
              <EmptyBox height={200} icon={<BiError />}>
                <Text>데이터를 가져오는데 실패함</Text>
              </EmptyBox>
            }
          >
            <Suspense
              fallback={
                <EmptyBox height={200}>
                  <Spinner />
                </EmptyBox>
              }
            >
              <HpSrIllData />
            </Suspense>
          </ErrorBoundary>
        </StyledHpSriIllContent>
      );
    }

    (좌) 에러 발생 시 (우) 로딩 시

    react query devtools를 사용한다면 각 query의 Actions을 활용하여 에러와 로딩 을 발생시킬 수 있습니다.

     

     

    지금까지 응급실 알리미 서비스의 에러 처리에 대해 알아보았습니다.

    에러가 발생했을때, 일관되게 대응 하는 것이 사용자의 불편을 최소화하고 신뢰감을 형성하는 데 큰 도움이 되기때문에 에러 설계는 중요하다고 생각합니다.

    에러가 발생할 수 있는 다양한 상황을 고려하여, 설계하는 것은 쉽지 않다는 것을 깨달았습니다. 현재 설계한 에러 상황은 필수적인 상황을 설계한 것으로 지속적인 개선이 필요하다고 생각합니다. 다음엔 500번대 에러와 다양항 HTTP메서드 에러에 대응하여 설계하고 싶습니다.


    참고 자료
    https://tkdodo.eu/blog/react-query-error-handling#the-global-callbacks

    https://tkdodo.eu/blog/react-query-error-handling

    댓글