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 호출이 실패하면:

  1. mutationFn 내에서 명시적으로 throw된 에러가 React Query에 의해 감지됨
  2. throwOnError: true 설정으로 인해 에러가 Error Boundary로 전파
  3. Error Boundary의 componentDidCatch에서 에러를 캐치하여 모달로 표시
  4. 사용자에게 에러 상황을 알려줌

제한사항 및 주의사항

ErrorBoundary가 캐치하지 못하는 상황들:

  1. 이벤트 핸들러 내부의 에러

    • onClick, onChange 등의 이벤트 핸들러에서 발생하는 에러는 Error Boundary가 직접 캐치할 수 없음
    • 해결 방법:
      1. try-catch로 직접 처리 (Error Boundary 사용하지 않음)
      2. try-catch로 잡은 후 state를 통해 다음 렌더링에서 throw하여 Error Boundary로 전파
    • 단순히 이벤트 핸들러에서 throw하는 것만으로는 Error Boundary가 캐치할 수 없음
  2. 비동기 코드의 에러

    • React Query의 throwOnError 옵션으로 해결
    • useQuery, useMutation의 모든 에러가 ErrorBoundary로 전파됨
  3. 서버 사이드 렌더링 중의 에러

    • Next.js의 error.tsx 활용 필요
  4. ErrorBoundary 자체의 에러

결론

권장 사항

에러의 성격에 따라 적절한 처리 방식을 선택하는 것이 좋습니다:

  1. API 요청 에러 (비동기 에러)

    • QueryClient에서 에러를 중앙화하여 처리
    • Error Boundary로 처리
  2. 렌더링 에러

    • Error Boundary로 처리

이렇게 에러의 성격에 맞게 처리 방식을 분리하면:

  • 더 명확한 에러 처리 흐름
  • 각 상황에 맞는 적절한 UX 제공
  • 유지보수하기 쉬운 코드 구조

이를 통해 더 선언적이고 관리하기 쉬운 에러 처리 시스템을 구축할 수 있습니다.


Error Boundary 사용 예시 코드는 여기로