전역 에러 바운더리: 앱 어디에, 어떻게 설정하나

Error Boundary를 어떻게 만드는지는 이전 아티클에서 다뤘습니다. 이번에는 실제 앱에서 어디에, 몇 개를, 어떻게 배치하는지 — 아키텍처 관점에서 봅니다.

배치 전략: 세 레벨

Error Boundary는 하나만 두는 게 아닙니다. 목적에 따라 레벨을 나눠서 중첩합니다.

main.jsx
└── <ErrorBoundary>          ← 레벨 1: 전역 (최후의 보루)
    └── <App>
        └── <Routes>
            └── <ErrorBoundary>   ← 레벨 2: 라우트 단위
                └── <PageComponent>
                    └── <ErrorBoundary>  ← 레벨 3: 컴포넌트 단위 (선택)
                        └── <Widget />

에러가 발생하면 가장 가까운 상위 Error Boundary가 잡습니다.


레벨 1: 전역 (main.jsx)

앱 전체를 감싸는 가장 바깥쪽 Error Boundary입니다. 어디서도 처리되지 않은 에러의 최후 방어선입니다.

// main.jsx
import { createRoot } from 'react-dom/client';
import { ErrorBoundary } from 'react-error-boundary';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
const queryClient = new QueryClient();
 
function GlobalErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div style={{ padding: '2rem', textAlign: 'center' }}>
      <h1>앱에 문제가 발생했습니다</h1>
      <p>{error.message}</p>
      <button onClick={resetErrorBoundary}>새로고침</button>
    </div>
  );
}
 
createRoot(document.getElementById('root')).render(
  <ErrorBoundary
    FallbackComponent={GlobalErrorFallback}
    onError={(error, info) => {
      // Sentry 같은 에러 추적 서비스에 전송
      console.error('[Global Error]', error, info.componentStack);
    }}
  >
    <QueryClientProvider client={queryClient}>
      <App />
    </QueryClientProvider>
  </ErrorBoundary>
);

QueryClientProvider는 Error Boundary 안쪽에 둡니다. 그래야 React Query 관련 에러도 같이 잡힙니다.


레벨 2: 라우트 단위 (App.jsx)

전역 Error Boundary만 있으면, 한 페이지에서 에러가 나도 앱 전체가 오류 화면으로 대체됩니다. 라우트마다 Error Boundary를 두면 에러가 해당 페이지에만 국한됩니다.

// App.jsx
import { Routes, Route } from 'react-router-dom';
import { ErrorBoundary } from 'react-error-boundary';
 
function PageErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <h2>이 페이지를 불러올 수 없습니다</h2>
      <p>{error.message}</p>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}
 
// 라우트를 Error Boundary로 감싸는 헬퍼
function WithErrorBoundary({ children }) {
  return (
    <ErrorBoundary FallbackComponent={PageErrorFallback}>
      {children}
    </ErrorBoundary>
  );
}
 
function App() {
  return (
    <Routes>
      <Route path="/" element={<WithErrorBoundary><HomePage /></WithErrorBoundary>} />
      <Route path="/dashboard" element={<WithErrorBoundary><DashboardPage /></WithErrorBoundary>} />
      <Route path="/profile" element={<WithErrorBoundary><ProfilePage /></WithErrorBoundary>} />
    </Routes>
  );
}

/dashboard에서 에러가 나도 //profile은 정상입니다.


레벨 3: 컴포넌트 단위 (선택)

중요도가 낮거나 독립적인 위젯에만 선택적으로 씁니다. 전체 페이지가 날아가는 것보다 해당 위젯만 에러 처리하는 게 나을 때입니다.

function DashboardPage() {
  return (
    <div>
      <MainContent />  {/* 실패하면 페이지 레벨 ErrorBoundary가 잡음 */}
 
      {/* 이 위젯은 실패해도 나머지 대시보드는 정상이어야 함 */}
      <ErrorBoundary fallback={<p>추천 콘텐츠를 불러올 수 없습니다.</p>}>
        <RecommendationWidget />
      </ErrorBoundary>
    </div>
  );
}

React Query와 연동

React Query는 기본적으로 에러를 isError 상태로 처리합니다. throwOnError: true를 주면 Error Boundary로 에러를 넘깁니다.

// 특정 쿼리에서만 Error Boundary 사용
useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  throwOnError: true, // → 가장 가까운 ErrorBoundary가 잡음
});
 
// QueryClient 기본값으로 전체 적용
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: true,
    },
  },
});

throwOnError: true를 전체 기본값으로 설정하면 모든 쿼리 에러가 Error Boundary로 올라갑니다. useQuery 개별 옵션으로 throwOnError: false를 주면 특정 쿼리만 예외처리할 수 있습니다.

Suspense + Error Boundary 조합

React Query의 suspense: true와 함께 쓰면 로딩과 에러를 컴포넌트 바깥에서 선언적으로 처리합니다.

function UserProfile() {
  // Suspense가 로딩을 처리, ErrorBoundary가 에러를 처리
  // 이 컴포넌트 안에서는 로딩/에러 분기 없이 데이터만 씀
  const { data: user } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    suspense: true,
    throwOnError: true,
  });
 
  return <div>{user.name}</div>;
}
 
// 사용 측
function App() {
  return (
    <ErrorBoundary FallbackComponent={PageErrorFallback}>
      <Suspense fallback={<Spinner />}>
        <UserProfile />
      </Suspense>
    </ErrorBoundary>
  );
}

Sentry 연동

에러 추적 서비스와 연동할 때는 onError 콜백에서 전송합니다.

import * as Sentry from '@sentry/react';
 
Sentry.init({ dsn: 'YOUR_DSN' });
 
createRoot(document.getElementById('root')).render(
  <ErrorBoundary
    FallbackComponent={GlobalErrorFallback}
    onError={(error, info) => {
      Sentry.captureException(error, {
        contexts: { react: { componentStack: info.componentStack } },
      });
    }}
  >
    <App />
  </ErrorBoundary>
);

혹은 @sentry/reactSentry.ErrorBoundary를 바로 사용할 수 있습니다.

import { ErrorBoundary } from '@sentry/react';
 
createRoot(document.getElementById('root')).render(
  <ErrorBoundary fallback={<GlobalErrorFallback />} showDialog>
    <App />
  </ErrorBoundary>
);

showDialog: true를 주면 에러 발생 시 사용자에게 피드백 입력 다이얼로그를 자동으로 띄웁니다.


정리

레벨위치목적fallback
전역main.jsx최후의 보루, 에러 추적앱 전체 오류 페이지
라우트App.jsx페이지 단위 격리”이 페이지를 불러올 수 없습니다”
컴포넌트필요한 곳위젯 단위 격리인라인 에러 메시지

전역 Error Boundary 하나만으로는 부족합니다. 라우트 단위로 에러를 격리해야 한 페이지의 에러가 앱 전체를 망가뜨리지 않습니다.