Error Boundary를 적용해 오류 처리하기
이 글에서 소개하는 에러 처리 방식은 제가 선택한 하나의 접근 방식일 뿐, 모든 상황에서 최선의 방법이 아닐 수 있습니다.
React의 Error Boundary와 React Query를 활용한 효과적인 에러 처리 시스템 구축 경험을 공유합니다.
Error Boundary란?
Error Boundary는 React 16에서 도입된 기능으로, 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 캐치하고 처리할 수 있는 React 컴포넌트입니다. 클래스 컴포넌트에서만 직접 구현 가능하며, 렌더링 에러를 처리하려면 getDerivedStateFromError
를, 그 외의 에러는 componentDidCatch
를 사용합니다.
에러 처리 방식 선택하기
React Query를 사용할 때 에러를 처리하는 방식은 크게 두 가지가 있습니다:
1. QueryProvider에서 직접 처리
const QueryProvider = ({ children }: { children: ReactNode }) => {
const { showModal } = useModal();
const [queryClient] = useState(
new QueryClient({
defaultOptions: {
mutations: {
onError: (error: Error & { title?: string }) => {
showModal(error.message);
},
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
이 방식의 장점:
- 비동기 에러를 직접 모달로 표시 가능
- 대체 UI 없이도 에러 처리 가능
- 구현이 간단함
2. Error Boundary로 전파
const QueryProvider = ({ children }: { children: ReactNode }) => {
const [queryClient] = useState(
new QueryClient({
defaultOptions: {
mutations: {
throwOnError: true,
},
queries: {
throwOnError: true,
},
},
})
);
return (
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
};
이 방식의 장점:
- React의 선언적 에러 처리 방식 활용
- 렌더링 에러도 함께 처리 가능
Error Boundary 구현
비동기 에러만 처리하는 경우
"use client";
import { Component, ReactNode } from "react";
import { useModal } from "@/providers/modal-provider";
interface Props {
children: ReactNode;
}
class ErrorBoundaryClass extends Component<
Props & { showModal: (title: string, message: string) => void }
> {
componentDidCatch(error: Error & { title?: string }) {
this.props.showModal(
error.title,
error.message,
);
}
render() {
return this.props.children;
}
}
export function ErrorBoundary({ children }: Props) {
const { showModal } = useModal();
return (
<ErrorBoundaryClass showModal={showModal}>{children}</ErrorBoundaryClass>
);
}
렌더링 에러도 처리하는 경우
class ErrorBoundaryClass extends Component<Props> {
state = { hasError: false };
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error: Error & { title?: string }) {
this.props.showModal(
error.title,
error.message,
);
}
render() {
if (this.state.hasError) {
return <div>Error</div>;
}
return this.props.children;
}
}
앱 전체 래핑
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ModalProvider>
<ErrorBoundary>
<QueryProvider>{children}</QueryProvider>
</ErrorBoundary>
</ModalProvider>
);
}
실제 사용 예시
const PostForm = () => {
const mutation = useMutation({
mutationFn: async (newPost: Post) => {
try {
const response = await fetch("/api/posts", {
method: "POST",
body: JSON.stringify(newPost),
headers: {
"Content-Type": "application/json",
},
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message);
}
return response.json();
} catch (error) {
throw new Error(error.message);
}
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
mutation.mutate({
title: "새 게시글",
content: "내용...",
});
};
return (
<form onSubmit={handleSubmit}>
<button type="submit">저장</button>
</form>
);
};
폼 제출 후 API 호출이 실패하면:
- mutationFn 내에서 명시적으로 throw된 에러가 React Query에 의해 감지됨
- throwOnError: true 설정으로 인해 에러가 Error Boundary로 전파
- Error Boundary의 componentDidCatch에서 에러를 캐치하여 모달로 표시
- 사용자에게 에러 상황을 알려줌
제한사항 및 주의사항
ErrorBoundary가 캐치하지 못하는 상황들:
-
이벤트 핸들러 내부의 에러
- onClick, onChange 등의 이벤트 핸들러에서 발생하는 에러는 Error Boundary가 직접 캐치할 수 없음
- 해결 방법:
- try-catch로 직접 처리 (Error Boundary 사용하지 않음)
- try-catch로 잡은 후 state를 통해 다음 렌더링에서 throw하여 Error Boundary로 전파
- 단순히 이벤트 핸들러에서 throw하는 것만으로는 Error Boundary가 캐치할 수 없음
-
비동기 코드의 에러
- React Query의 throwOnError 옵션으로 해결
- useQuery, useMutation의 모든 에러가 ErrorBoundary로 전파됨
-
서버 사이드 렌더링 중의 에러
- Next.js의 error.tsx 활용 필요
-
ErrorBoundary 자체의 에러
결론
권장 사항
에러의 성격에 따라 적절한 처리 방식을 선택하는 것이 좋습니다:
-
API 요청 에러 (비동기 에러)
- QueryClient에서 에러를 중앙화하여 처리
- Error Boundary로 처리
-
렌더링 에러
- Error Boundary로 처리
이렇게 에러의 성격에 맞게 처리 방식을 분리하면:
- 더 명확한 에러 처리 흐름
- 각 상황에 맞는 적절한 UX 제공
- 유지보수하기 쉬운 코드 구조
이를 통해 더 선언적이고 관리하기 쉬운 에러 처리 시스템을 구축할 수 있습니다.