본문 바로가기

React

React에서 Error Boundary로 오류 처리하기

프로덕션에서 갑작스런 빈 화면을 막고 사용자에게 의미 있는 대체 UI를 보여주려면 Error Boundary가 필수입니다. Error Boundary는 하위 트리 렌더링 중 발생한 오류를 잡아 폴백 UI로 전환하고, 로깅까지 연결할 수 있는 안전망입니다.

1. Error Boundary가 필요한 이유

- 컴포넌트 렌더/라이프사이클에서 발생한 오류로 전체 앱이 깨지는 것을 방지합니다.

- 사용자에게 폴백 UI(다시 시도, 고객센터 안내 등)를 즉시 제공합니다.

- 모니터링/로그 수집 도구(Sentry 등)와 연동해 장애 탐지 속도를 높입니다.

2. 기본 구현 (Class 기반)

import React from 'react';

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }

  componentDidCatch(error, info) {
    // 여기에 로깅/모니터링 연동
    // logError(error, info);
    console.error('ErrorBoundary caught:', error, info);
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null });
    this.props.onReset?.();
  };

  render() {
    if (this.state.hasError) {
      return (
        

문제가 발생했습니다.

{String(this.state.error)}
); } return this.props.children; } } export default ErrorBoundary;

3. 어디에 감쌀까요?

- 전체 앱 루트: 치명적 오류에 대한 최후의 안전망

- 페이지/라우트 단위: 문제 구역만 격리

- 외부 의존도가 높은 위젯: 그래프, 광고, 서드파티 SDK

// 루트 또는 라우트 경계 예시
<ErrorBoundary onReset={() => window.location.reload()}>
  <App />
</ErrorBoundary>

4. Error Boundary가 잡지 못하는 오류

- 이벤트 핸들러 내부 오류

- 비동기 콜백/Promise 내부 오류

- 서버사이드 렌더링(SSR) 중 발생한 오류

이 경우 패턴을 통해 경계로 전달해야 합니다.

5. 비동기/이벤트 오류를 경계로 전달하는 패턴

import React from 'react';

function FetchUser({ id }) {
  const [data, setData] = React.useState(null);
  const [error, setError] = React.useState(null);

  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const res = await fetch(`/api/users/${id}`);
        if (!res.ok) throw new Error('사용자 정보를 불러오지 못했습니다');
        const json = await res.json();
        if (!cancelled) setData(json);
      } catch (e) {
        if (!cancelled) setError(e);
      }
    })();
    return () => { cancelled = true; };
  }, [id]);

  // Error Boundary로 승격
  if (error) throw error;
  if (!data) return <div>로딩...</div>;
  return <div>{data.name}</div>;
}

function DangerousButton() {
  const [error, setError] = React.useState(null);
  const handleClick = async () => {
    try {
      // 위험한 비동기 로직
      await doSomethingRisky();
    } catch (e) {
      setError(e);
    }
  };
  if (error) throw error; // 경계로 전달
  return <button onClick={handleClick}>실행</button>;
}

6. 리셋 전략: stuck 상태에서 빠져나오기

- 버튼으로 상태 초기화: 위의 handleReset처럼 hasError를 false로

- key 변경으로 강제 재마운트: 라우트/파라미터 변화에 따라 ErrorBoundary에 key 부여

import { useLocation } from 'react-router-dom';

function RouteBoundary({ children }) {
  const location = useLocation();
  return (
    <ErrorBoundary key={location.pathname}>
      {children}
    </ErrorBoundary>
  );
}

7. 모니터링 도구 연동(Sentry 예시)

import * as Sentry from '@sentry/react';

class ErrorBoundary extends React.Component {
  // ...생략
  componentDidCatch(error, info) {
    Sentry.captureException(error, { extra: info });
  }
  // ...생략
}

8. 라이브러리 사용: react-error-boundary

클래스를 직접 만들기 번거롭다면 검증된 라이브러리를 사용합니다. resetKeys로 자동 리셋, onReset로 정리 로직을 쉽게 붙일 수 있습니다.

import { ErrorBoundary } from 'react-error-boundary';

function Fallback({ error, resetErrorBoundary }) {
  return (
    <div role="alert">
      <p>문제가 발생했습니다.</p>
      <pre>{String(error)}</pre>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}

function Page({ userId }) {
  return (
    <ErrorBoundary
      FallbackComponent={Fallback}
      onError={(error, info) => console.log('log', error, info)}
      onReset={() => {/* 캐시 초기화 등 */}}
      resetKeys={[userId]} // userId 바뀌면 자동 리셋
    >
      <FetchUser id={userId} />
    </ErrorBoundary>
  );
}

9. Suspense와 함께 쓰기

데이터 로딩은 Suspense, 오류는 Error Boundary가 담당하게 배치합니다.

<ErrorBoundary FallbackComponent={Fallback}>
  <React.Suspense fallback={<div>로딩...</div>}>
    <UserProfile />
  </React.Suspense>
</ErrorBoundary>

10. 한계와 체크리스트

- Error Boundary는 렌더/라이프사이클/하위 트리에서 발생한 오류만 잡습니다. 이벤트/비동기는 위 패턴으로 throw하여 승격하세요.

- SSR 오류는 서버에서 try/catch로 처리하고, 클라이언트 전환 후엔 Boundary를 사용하세요.

- 너무 큰 범위를 한 경계로 감싸면 작은 오류에도 큰 UI가 사라집니다. 기능 단위로 잘게 나누세요.

- 경계가 폴백 상태에 고착되지 않도록 리셋 경로를 반드시 제공합니다(버튼, resetKeys, key 변경).

- 로깅은 민감정보를 제외하고 수집하며, 사용자 행동 컨텍스트(페이지, 파라미터)를 함께 기록하면 원인 분석이 빨라집니다.

실무 팁: 라우트 경계 + 위젯 경계를 병행하고, 비동기 오류는 "state에 담고 render에서 throw" 패턴으로 일관되게 경계로 올리면 유지보수가 쉬워집니다.