리액트 쿼리 에러 핸들링

원문: TkDodo, "React Query Error Handling"
오류 처리는 비동기 데이터 특히 데이터를 가져오는 작업을 할 때 필수적인 부분입니다. 모든 요청이 성공하는 것은 아니며 모든 프로미스가 이행되는 것도 아닙니다.
하지만 처음부터 오류 처리에 집중하지 않는 경우가 많습니다. "성공적인 경우"를 먼저 생각하고 오류 처리는 나중에 생각나서 하게 됩니다.
그러나 오류 처리 방법을 생각하지 않는 것은 사용자 경험에 부정적인 영향을 미칠 것입니다. 이런 상황을 피하고자 리액트 쿼리가 제공하는 오류 처리 옵션에 대해 자세히 알아봅시다.
전제 조건
리액트 쿼리는 오류를 올바르게 처리하기 위해 거부된 프로미스가 필요합니다. 다행히도 axios와 같은 라이브러리를 사용할 때 거부된 프로미스를 받을 수 있습니다.
4xx, 5xx와 같은 오류 상태 코드에 대한 거부된 프로미스를 제공하지 않는 fetch API나 다른 라이브러리로 작업하는 경우 queryFn에서 직접 변환해야 합니다. 이는 공식 문서에서 다루고 있습니다.
기본 예제
오류를 나타내는 대부분의 예제가 어떤 모습인지 살펴보겠습니다.
function TodoList() {
const todos = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
})
if (todos.isPending) {
return 'Loading...'
}
// ✅ 일반적인 오류 처리
// 아래 조건을 todos.status === 'error'로 확인할 수도 있습니다.
if (todos.isError) {
return 'An error occurred'
}
return (
<div>
{todos.data.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</div>
)
}
위에서는 리액트 쿼리에서 제공하는 status enum으로부터 파생된 isError boolean 플래그로 오류 상황을 처리하고 있습니다.
몇몇 특정 상황에서는 괜찮지만 몇 가지 문제점이 있습니다.
백그라운드 오류를 제대로 다루고 있지 않습니다. 단지 백그라운드의 데이터 다시 가져오기가 실패한다고 Todo List를 완전히 언마운트하고 싶을까요? 아마도 api가 일시적으로 다운되거나 요청 제한에 도달하거나 하는 경우 몇 분 내로 다시 동작할 것입니다. 이 상황을 개선할 방법을 찾고자 한다면 #4: Status Checks in React Query에서 자세히 알아보세요.
쿼리를 사용하는 모든 컴포넌트에서 위 코드를 작성하는 것은 꽤 반복해서 작성하게 됩니다.
두 번째 문제를 해결하기 위해서 리액트 자체에서 제공하는 좋은 기능을 사용할 수 있습니다.
에러 바운더리
에러 바운더리는 일반적으로 렌더링 도중 발생하는 런타임 에러를 잡기 위해 사용되는 개념입니다. 에러 바운더리는 오류에 적절하게 반응하고 폴백 UI를 보여주도록 합니다.
원하는 세부 수준에서 에러 바운더리를 감싸 나머지 UI가 해당 오류의 영향을 받지 않도록 할 수 있기 때문에 유용합니다.
에러 바운더리는 렌더링 중에 발생하지 않는 비동기 오류는 잡지 못합니다. 리액트 쿼리에서 에러 바운더리가 동작하게 만들려면 라이브러리가 내부적으로 에러를 잡고 다음 렌더링 사이클에서 에러를 다시 던지도록 함으로써 에러 바운더리에 포착되도록 만듭니다.
이 방식은 오류 처리에 대해 매우 천재적이면서도 간단한 접근이라고 생각하며 이를 작동시키기 위해서는 쿼리에 throwOnError 플래그를 전달하거나 기본 설정을 통해 제공하기만 하면 됩니다.
function TodoList() {
// ✅ 가장 가까운 에러 바운더리로
// 모든 데이터 가져오기 오류를 전파시킬 것입니다.
const todos = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
throwOnError: true,
})
if (todos.data) {
return (
<div>
{todos.data.map((todo) => (
<Todo key={todo.id} {...todo} />
))}
</div>
)
}
return 'Loading...'
}
v3.23.0부터는 throwOnError에 함수를 제공하여 어떤 오류를 에러 바운더리로 보낼지 또는 지역에서 처리할지 사용자화할 수도 있습니다.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 🚀 서버 오류에 대해서만 에러 바운더리로 전파됩니다.
throwOnError: (error) => error.response?.status >= 500,
})
뮤테이션에도 적용되며 양식 제출을 할 때 매우 유용합니다. 백엔드에서의 유효성 검사 실패와 같은 4xx 오류는 지역적으로 다루고 모든 5xx 서버 오류는 에러 바운더리로 전파할 수 있습니다.
업데이트
v5 이전에는
throwOnError플래그는useErrorBoundary였습니다.
오류 알림 표시하기
일부 사용 사례에서는 화면에 오류 알림 배너를 보여주는 것보다 어딘가에 팝업되고 자동으로 사라지는 토스트 알림을 보여주는 것이 낫기도 합니다. 일반적으로 react-hot-toast에서 제공하는 것과 같은 명령형 api로 열립니다.
import toast from 'react-hot-toast'
toast.error('Something went wrong')
그렇다면 리액트 쿼리에서 오류가 발생했을 때 어떻게 해야 할까요?
onError 콜백
업데이트
onError와onSuccess콜백은 v5의 useQuery에서 제거되었습니다. 자세한 이유는 링크에서 확인할 수 있습니다.
const useTodos = () =>
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// ⚠️ 좋아 보이기는 하지만 정말 원하던 것은 _아닐수도_ 있습니다.
onError: (error) =>
toast.error(`Something went wrong: ${error.message}`),
})
언뜻 보기에 onError 콜백은 데이터 가져오기가 실패한 경우 부수 효과(side effect)를 수행하는 데 필요한 것처럼 보입니다. 그리고 커스텀 훅을 한 번만 사용하는 한 작동할 것입니다!
보시다시피 useQuery의 onError 콜백은 모든 Observer마다 호출됩니다. 즉, 애플리케이션에서 useTodos를 두 번 호출하면 한 번의 네트워크 요청 실패에도 두 번의 에러 토스트가 나타나게 됩니다.
개념적으로 onError 콜백이 useEffect와 비슷하다고 생각할 것입니다. 위 예시를 useEffect 구문으로 확장해서 생각해 보면 사용하는 곳 모두에서 실행된다는 것이 더욱 분명해집니다.
const useTodos = () => {
const todos = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
})
// 🚨 이 커스텀 훅을 사용하는 모든 컴포넌트에서
// 개별적으로 이펙트가 실행됩니다.
React.useEffect(() => {
if (todos.error) {
toast.error(`Something went wrong: ${todos.error.message}`)
}
}, [todos.error])
return todos
}
물론 커스텀 훅에 콜백을 추가하지 않고 훅을 호출하는 곳에 콜백을 추가한다면 전혀 문제가 되지 않습니다. 하지만 모든 옵서버에 데이터 가져오기가 실패했다고 알리지 않고 사용자에게 한 번만 알리고 싶다면 어떻게 하죠? 이를 위해 리액트 쿼리는 다른 계층의 콜백이 있습니다.
전역 콜백
전역 콜백은 QueryCache를 만들 때 제공되어야 하며 새로운 QueryClient를 만들 때 암시적으로 제공되지만 사용자 지정을 할 수도 있습니다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
toast.error(`Something went wrong: ${error.message}`),
}),
})
이제 각 쿼리에 대해 오류 토스트를 한 번만 표시하며 이는 정확히 우리가 원하던 것입니다.🥳 또한 요청당 한 번만 실행이 보장되고 defaultOptions처럼 덮어쓸 수 없기 때문에 수행하려는 모든 종류의 오류 추적 또는 모니터링을 배치하기에 가장 적합한 장소일 수 있습니다.
모두 종합하기
리액트 쿼리에서 오류를 처리하는 세 가지 주요 방법은 아래와 같습니다.
useQuery로부터 반환되는
error프로퍼티쿼리 자체 또는 전역 QueryCache나 MutationCache에 있는
onError콜백에러 바운더리 사용하기
원하는 대로 섞어서 조합할 수 있으며 개인적으로는 백그라운드에서 데이터를 다시 가져오는 작업에는 오래된 UI를 온전하게 보존하기 위해 오류 토스트를 보여주고 다른 것들에는 지역적으로 처리하거나 에러 바운더리로 처리하는 것을 선호합니다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// 🎉 데이터가 캐시에 이미 존재한다면
// 백그라운드 업데이트 실패했다는 것을 나타내므로 오류 토스트만 보여줍니다.
if (query.state.data !== undefined) {
toast.error(`Something went wrong: ${error.message}`)
}
},
}),
})




