1. 들어가기
에러 핸들링은 비동기 작업(데이터 fetch)에서 필수적이다.
Request가 항상 성공하는 것이 아니기 떄문이다.
React Query가 오류처리를 위해 어떤 옵션을 제공하는지 알아보자.
1.1. 전제조건
React Query가 에러를 핸들링하려면 rejected Promise가 필요하다.
axios 라이브러리는 4xx 나 5xx 상태코드에서 rejected Promise를 사용해서 문제가 없겠지만,
fetch는 4xx나 5xx에서 rejected Promise를 제공하지 않기 때문에, queryFn에서 직접 변환해야한다.
이 부분은 공식문서에서 찾아볼 수 있다.
2. 기본예시
function TodoList() {
const todos = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
})
if (todos.isPending) {
return 'Loading...'
}
// ✅ 기본 에러핸들링
// 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` 를 이용해서 에러핸들링을 한다.
몇몇 시나리오에서는 문제가 없지만, 몇 가지 단점들이 존재한다.
1. 백그라운드에서 발생하는 에러를 제대로 핸들링하지 못한다.
예를 들어, 단지 백그라운드 리페칭이 실패했다고 해서 한번 렌더링 되어있는 투두리스트 컴포넌트를 unmount 하고싶지는 않을것이다.
그 api가 임시로 내려갔을수도 있고, 제한요청을 초과했을 수도 있다.
이 상황을 잘 풀어나가려면 Status Checks in React Query를 참고하자.
2. 쿼리를 사용하는 모든곳에서 저런 에러처리를 한다면 보일러플레이트 마냥 반복적이게 보일 수 있다.
2번 문제를 해결하기 위해서, 에러바운더리를 사용할 수 있다.
2. 에러바운더리
에러바운더리를 이용해 렌더링 중에 일어나는 런타임 에러를 catch하고 대체 fallback UI를 보여줄 수 있다.
원하는 경계(세부 컴포넌트들의 경계)를 에러바운더리로 감싸면 그 경계 외부의 나머지 UI가 해당 오류의 영향을 받지 않을 수 있어서 아주 좋다.
다만 비동기 에러는 렌더링 중에 발생하지 않기 때문에(useEffect 에서 fetching 한다) 에러바운더리가 잡지 못한다.
React Query에서는 비동기 상황에서도 에러바운더리가 동작하도록 하는 throwOnError 옵션을 제공한다.
이는 다음 렌더링 사이클에서 에러바운더리가 해당 에러를 잡을 수 있도록 React query 내부적으로 받은 에러를 다시 throw하는 역할을 한다.
function TodoList() {
// ✅ 가장 가까운 에러바운더리에 fetching 에러를 던진다.
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...'
}
어떤 에러를 다시 던질지, 아니면 그곳에서 처리할지 결정할 수도 있다.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// 🚀 500에러만 에러바운더리로 던진다.
throwOnError: (error) => error.response?.status >= 500,
})
이는 mutations에서도 동작하고, 폼 제출할때 꽤 유용하다.
4xx 에러는 로컬에서 처리할 수 있지만 (예: 일부 백엔드 유효성 검사에 실패한 경우),
5xx 에러는 에러바운더리로 전달할 수 있다.
3. 에러 알림 보여주기
몇몇 사례에서는, 에러를 토스트 메시지로 보여주는게 좋다.
보통 토스트 기능은 명령형으로 구현하는 경우가 많다.
import toast from 'react-hot-toast'
toast.error('Something went wrong')
onError 콜백
먼저 useQuery의 onError와 onSuccess 콜백은 v5에서 삭제되었음을 알린다.
const useTodos = () =>
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
// ⚠️ 좋아보이지만, 원하는 방식으로 동작하지 않을 수 있다.
onError: (error) =>
toast.error(`Something went wrong: ${error.message}`),
})
onError 콜백은 fetch가 실패했을때 side effect를 수행하기에 좋아보이고, 실제로 동작한다. (그 커스텀 훅을 한번만 사용했을때 한해서!)
만약 `useTodos` 를 현재 보이는 화면에서 두번이상 호출한다면, 요청은 한번만 했음에도 에러 토스트는 두개가 렌더링될 것이다.
좀 더 이해를 편하기 위해 onError 콜백이 useEffect에서 호출하는 함수와 같다.
const useTodos = () => {
const todos = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos
})
// 🚨 effects는 이 훅을 사용하는 모든 컴포넌트에서 호출된다(!)
React.useEffect(() => {
if (todos.error) {
toast.error(`Something went wrong: ${todos.error.message}`)
}
}, [todos.error])
return todos
}
물론 useTodos 같은 커스텀 훅에 에러핸들링 콜백을 추가하지 않고
훅을 호출하는 컴포넌트 내부에서 useEffect를 이용해 에러핸들링 콜백을 추가하면 위 문제는 해결할 수 있다.
하지만 React Query onError 콜백을 사용하고 싶은데, 사용자에게 fetch가 실패했음을 한번만 알리고 싶다면 어떻게 해야할까?
전역 콜백
전역 콜백은 QueryCache를 생성하면서 함께 넘겨줄 수 있다.
QueryCache는 new QueryClient를 만들때 암묵적으로 생성되지만, 커스텀할 수도 있다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error) =>
toast.error(`Something went wrong: ${error.message}`),
}),
})
이제 각 쿼리에 대해 오류 토스트가 한 번만 표시된다.
즉 onError 콜백은 요청당 한 번만 실행되고 기본 옵션처럼 덮어쓸 수 없기 때문에,
모든 종류의 에러 추적 및 모니터링을 넣기에 가장 적합한 장소이다. (sentry를 사용해보지는 않았지만, 여기에 선언하면 좋을듯 하다)
참고로 mutations는 `MutationCache` 가 있다.
4. 정리
React Query에서 에러핸들링을 하는 3가지 주요 방식은 다음과 같다.
- useQuery에서 반환하는 `error`, `isError` property
- `onError` 콜백 (각 쿼리나 전역 QueryCache, MutationCache)
- 에러바운더리
Tkdodo는 백그라운드 리페칭에 대해서는 에러 토스트를 표시하고(오래된 UI를 그대로 유지하기 위해),
그 외에는 로컬 또는 에러바운더리를 사용하여 핸들링하는걸 좋아한다고 한다.
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error, query) => {
// 🎉 이미 페칭했던 데이터가 케시에 남아있을때만 에러를 보여준다.(query.state.data !== undefined)
// 즉 백그리운드 업데이트가 실패했을때를 의미한다.
if (query.state.data !== undefined) {
toast.error(`Something went wrong: ${error.message}`)
}
},
}),
})
Reference
https://tkdodo.eu/blog/react-query-error-handling 의 개인정리본입니다.
'React Query' 카테고리의 다른 글
6. React Query가 필요한 이유 (0) | 2024.07.21 |
---|---|
5. React Query의 Status (0) | 2024.02.21 |
4. 효율적으로 React Query Key 선언하기 (5) | 2024.02.14 |
2. Form에서 React Query 잘 활용하기 (0) | 2024.01.22 |
1. 실용적인 React Query (0) | 2024.01.10 |