본문 바로가기

FrontEnd

리액트 쿼리 : 에러 처리

반응형

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

 

React Query Error Handling

After covering the sunshine cases of data fetching, it's time to look at situations where things don't go as planned and "Something went wrong..."

tkdodo.eu

서론

리액트 쿼리는 비동기 상태 관리 라이브러리입니다.
오류 처리는 비동기적 데이터 가져오기 작업의 필수적인 부분입니다.
모든 요청이 성공하는 것은 아니며 모든 프로미스가 fullfilled되는 것도 아닙니다.
 
우리는 보통 처음에 성공을 가정하고 구현합니다.
그러나 에러를 처리하는 방법에 대해 생각하지 않으면 사용자 경험에 부정적인 영향을 미칠 수 있습니다.
에러 처리를 위해 React Query가 제공하는 옵션에 대해 알아보겠습니다.
 

사전 지식

React Query는 오류를 올바르게 처리하기 위해 rejected된 Promise가 필요합니다.
운 좋게도 axios와 같은 라이브러리로 작업할 때 바로 얻을 수 있는 것입니다.
 
기본 fetch api(the fetch API)와 같이,
4xx 또는 5xx와 같은 잘못된 상태 코드에 대해 rejected 프라미스를 제공하지 않는 API 또는 기타 라이브러리로 작업하는 경우
queryFn에서 직접 변환을 수행해야 합니다.
이것은 공식 문서(the official docs)에서 다룹니다.

표준 예제

대부분 에러 처리는 다음처럼 구현합니다.
Copythe-standard-example: copy code to clipboard
function TodoList() {
  const todos = useQuery(['todos'], fetchTodos)

  if (todos.isLoading) {
    return 'Loading...'
  }

  // ✅ standard error handling
  // could also check for: todos.status === 'error'
  if (todos.isError) {
    return 'An error occurred'
  }

  return (
    <div>
      {todos.data.map((todo) => (
        <Todo key={todo.id} {...todo} />
      ))}
    </div>
  )
}
React Query의 isError boolean 플래그를 확인하여 오류 상황을 처리합니다.
이것은 일부 시나리오에서는 확실히 괜찮지만 몇 가지 단점도 있습니다.
 

1. 백그라운드 오류를 잘 처리하지 못합니다.

백그라운드 다시 가져오기가 실패했기 때문에 전체 Todo 목록을 마운트 해제하고 싶습니까?
API가 일시적으로 다운되었거나 ,속도 제한에 도달했을 수 있습니다.
이 경우 몇 분 안에 다시 작동할 수 있습니다. 해당 상황을 개선하는 방법을 알아보려면 #4: Status Checks in React Query를 참조하세요.
 
 

2. 쿼리를 사용하는 컴포넌트마다 보일러플레이트가 쌓입니다.

 
두 번째 문제를 해결하기 위해 React 자체에서 직접 제공하는 훌륭한 기능을 사용할 수 있습니다.

Error Boundaries

에러 바운더리(Error Boundaries)라는, 렌더링 중에 발생하는 런타임 오류를 포착하기 위한 React의 개념을 사용하면

에러 발생 시 대체 UI를 표시할 수 있습니다.

 

우리가 원하는 크기의 컴포넌트 단위를 래핑할 수 있으므로 나머지 UI가 해당 오류의 영향을 받지 않기 때문에 좋습니다.

 

Error Boundaries가 할 수 없는 일이 있습니다.

렌더링 중에 발생하지 않는 비동기 오류를 잡는 것입니다.

따라서 React Query에서 Error Boundaries가 작동하도록 하기 위해 라이브러리는 내부적으로 오류를 포착하고,

다음 렌더링 주기에서 에러 바운더리가 이를 캐치할 수 있도록 다시 던집니다.

 

나는 이것이 오류 처리에 대한 매우 천재적이며 간단한 접근 방식이라고 생각합니다.

이 작업을 수행하려면 useErrorBoundary 플래그를 쿼리에 전달하거나 기본 설정을 통해 제공하기만 하면 됩니다.

function TodoList() {
  // ✅ will propagate all fetching errors to the nearest Error Boundary
  // ✅ 모든 fetching 오류를 가장 가까운 에러바운더리로 전파합니다.
  const todos = useQuery(['todos'], fetchTodos, { useErrorBoundary: true })

  if (todos.data) {
    return (
      <div>
        {todos.data.map((todo) => (
          <Todo key={todo.id} {...todo} />
        ))}
      </div>
    )
  }

  return 'Loading...'
}

v3.23.0부터는 useErrorBoundary에 함수를 제공하여

  • 어떤 오류는 Error Boundary로 보내고
  • 어떤 오류는 로컬에서 처리할지

선택할 수 있습니다.

useQuery(['todos'], fetchTodos, {
  // 🚀 only server errors will go to the Error Boundary
  useErrorBoundary: (error) => error.response?.status >= 500,
})

이것은 mutations에도 적용되며 양식 제출을 수행할 때 매우 유용합니다.

4xx 범위의 오류는 로컬에서 처리할 수 있으며(예: 일부 백엔드 유효성 검사가 실패한 경우),

모든 5xx 서버 오류는 에러바운더리로 전파될 수 있습니다.


오류 알림 표시

일부 사용 사례의 경우 화면에 경고 배너를 렌더링하는 대신 오류 토스트 알림을 표시하는 것이 더 나을 수 있습니다.
아래는 일반적으로 react-hot-toast에서 제공하는 것과 같은 명령형 API입니다.
import toast from 'react-hot-toast'

toast.error('Something went wrong')​
React Query에서 오류가 발생할 때 해당 라이브러리를 어떻게 사용할까용?

The onError callback

const useTodos = () =>
  useQuery(['todos'], fetchTodos, {
    // ⚠️ 좋아보이지만 당신이 원하는 것이 아닐 수 있어요.
    onError: (error) =>
      toast.error(`Something went wrong: ${error.message}`),
  })
onError 콜백은 가져오기가 실패할 경우 사이드 이펙트를 수행하는 데 필요한 것과 정확히 같으며
사용자 정의 훅 호출 하나 당 한번 호출됩니다.
 
즉, useQuery의 onError 콜백은 모든 Observer에 대해 호출됩니다.
즉, 애플리케이션에서 useTodos를 두 번 호출하면 하나의 네트워크 요청만 실패하더라도 두 개의 오류 알림을 받게 됩니다.
 
즉, 개념적으로 onError 콜백이 useEffect와 유사한 기능을 한다고 상상할 수 있습니다.
위의 예를 아래와 같은 구문으로 생각하면 이것이 모든 훅을 호출하는 컴포넌트에서 실행된다는 것이 더 분명해집니다.
CopyuseEffect-error-toast: copy code to clipboard
const useTodos = () => {
  const todos = useQuery(['todos'], fetchTodos)

  // 🚨 이펙트는 이 커스텀 훅을 사용하는
  // 모든 컴포넌트에서 각각 실행됩니다.
  React.useEffect(() => {
    if (todos.error) {
      toast.error(`Something went wrong: ${todos.error.message}`)
    }
  }, [todos.error])

  return todos
}​

물론 사용자 정의 훅(ex:useTodos)에 콜백을 추가하지 않고,
useQuery훅 호출에 추가하는 경우에는 전혀 문제가 없습니다.
 
하지만 모든 옵저버(컨슈머;컴포넌트)에게 가져오기가 실패했음을 알리고 싶은게 아니라
사용자에게 알리고 싶은 것이라면, 어떻게 해야 할까요?
이를 위해 React Query에는 다른 수준의 콜백이 있습니다.

글로벌 콜백

새 QueryClient를 만들 때 암시적으로 발생하는 QueryCache 필드에 전역 콜백을 제공합니다.

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error) =>
      toast.error(`Something went wrong: ${error.message}`),
  }),
})
해당 위치는 모든 종류의 오류 추적 또는 모니터링을 수행하기에 가장 좋은 곳입니다.
  • 이제 각 쿼리에 대해 한 번만 오류 토스트를 표시하며
  • 해당 설정을 덮어쓸 수 없습니다.

요약

React Query에서 오류를 처리하는 세 가지 주요 방법은 다음과 같습니다.
 

1. useQuery에서 반환된 error 프로퍼티

별로 추천하지 않습니다

2. onError 콜백(쿼리 자체 또는 전역 QueryCache / MutationCache에서)

주로 4xx 오류는 로컬 콜백으로 처리합니다(5xx 에러는 useErrorBoundary 사용).

오류를 컴포넌트 각각에서 처리하지 않고 한번에 처리하거나, 사용자에게 알릴 목적이면

QueryCache / MutationCache 의 필드를 사용합니다.

3. ErrorBoundary 사용 

  • 오류를 캐치하여, 렌더링을 발생시켜 다음 렌더링 주기에서 에러 바운더리를 렌더링 합니다.
  • 대체 UI를 표시할 수 있을 때 사용합니다.
  • 저는 500번대 에러처리에 GlobalErrorBoundary를 활용한 적이 있습니다.
원하는대로 골라 섞어 쓸 수 있습니다.
아래 예시는 백그라운드 다시 가져오기(refetch)에 대한 오류 toast을 표시하고,(오래된 UI를 그대로 유지하기 위해),
로컬(각 컴포넌트)에서 또는 에러바운더리(컴포넌트 상단의)를 사용하여 다른 모든 것을 처리하는 것입니다.
const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onError: (error, query) => {
      // 🎉 이미 캐시에 데이터가 있는 경우에만 오류 알림을 표시합니다.
      // 이는 백그라운드 업데이트가 실패했음을 의미합니다.
      if (query.state.data !== undefined) {
        toast.error(`Something went wrong: ${error.message}`)
      }
    },
  }),
})​

 

반응형