본문 바로가기
Frontend2024년 10월 18일8분 읽기

React Suspense와 ErrorBoundary 조합 패턴

YS
김영삼
조회 380

선언적 비동기 상태 관리

React의 Suspense는 비동기 작업의 로딩 상태를, ErrorBoundary는 에러 상태를 선언적으로 처리합니다. 두 컴포넌트를 조합하면 try-catch-finally와 유사한 패턴을 JSX로 표현할 수 있습니다.

ErrorBoundary 구현

import React from 'react';

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

  static getDerivedStateFromError(error) {
    return { error };
  }

  componentDidCatch(error, errorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
    if (this.props.onError) {
      this.props.onError(error, errorInfo);
    }
  }

  reset = () => {
    this.setState({ error: null });
  };

  render() {
    if (this.state.error) {
      if (this.props.fallback) {
        return this.props.fallback({
          error: this.state.error,
          reset: this.reset
        });
      }
      return <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

Suspense + ErrorBoundary 조합

function App() {
  return (
    <ErrorBoundary fallback={({ error, reset }) => (
      <div className="error-container">
        <h2>오류가 발생했습니다</h2>
        <p>{error.message}</p>
        <button onClick={reset}>다시 시도</button>
      </div>
    )}>
      <Suspense fallback={<LoadingSpinner />}>
        <UserProfile userId={1} />
      </Suspense>
    </ErrorBoundary>
  );
}

// 중첩 Suspense로 세밀한 로딩 제어
function Dashboard() {
  return (
    <ErrorBoundary fallback={ErrorFallback}>
      <Suspense fallback={<HeaderSkeleton />}>
        <Header />
      </Suspense>
      <div className="grid">
        <Suspense fallback={<ChartSkeleton />}>
          <SalesChart />
        </Suspense>
        <ErrorBoundary fallback={ErrorFallback}>
          <Suspense fallback={<TableSkeleton />}>
            <RecentOrders />
          </Suspense>
        </ErrorBoundary>
      </div>
    </ErrorBoundary>
  );
}

React Query와 Suspense 통합

import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch('/api/users/' + userId).then(r => {
      if (!r.ok) throw new Error('사용자를 찾을 수 없습니다');
      return r.json();
    }),
  });

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

// 병렬 데이터 로딩
function UserDashboard({ userId }) {
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: fetchUser,
  });
  const { data: posts } = useSuspenseQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
  });

  return <div>{user.name}의 게시글: {posts.length}개</div>;
}

AsyncBoundary 유틸리티 컴포넌트

function AsyncBoundary({ children, pendingFallback, rejectedFallback }) {
  return (
    <ErrorBoundary fallback={rejectedFallback}>
      <Suspense fallback={pendingFallback}>
        {children}
      </Suspense>
    </ErrorBoundary>
  );
}

// 사용
<AsyncBoundary
  pendingFallback={<Skeleton />}
  rejectedFallback={({ error, reset }) => (
    <ErrorCard error={error} onRetry={reset} />
  )}
>
  <DataComponent />
</AsyncBoundary>
  • ErrorBoundary는 클래스 컴포넌트로만 구현 가능하지만, 래퍼 패턴으로 함수 컴포넌트처럼 사용합니다
  • Suspense 경계를 세밀하게 나누면 부분적 로딩 UI를 제공할 수 있습니다
  • 이벤트 핸들러 내 에러는 ErrorBoundary에서 잡히지 않으므로 별도 처리가 필요합니다
  • React 19에서는 use() 훅으로 Promise를 직접 Suspense와 통합할 수 있습니다

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록