Learning
레슨 11 / 12·6개 토픽

Error Boundary와 성능 최적화

Error Boundary란?

Error Boundary는 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 잡아내고, 에러 UI를 표시하는 React 패턴입니다. 현재 클래스 컴포넌트로만 구현할 수 있으며, react-error-boundary 라이브러리로 더 편리하게 사용합니다.

클래스 기반 Error Boundary

tsx
import { Component, ErrorInfo, ReactNode } from 'react';

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  error: Error | null;
}

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

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

  componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error('ErrorBoundary caught:', error, errorInfo);
    // 에러 리포팅 서비스로 전송 가능
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback || (
        <div className="p-4 bg-red-50 border border-red-200 rounded">
          <h2 className="text-red-600">문제가 발생했습니다.</h2>
          <p className="text-red-500">{this.state.error?.message}</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            다시 시도
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// 사용법
function App() {
  return (
    <ErrorBoundary fallback={<p>에러 발생!</p>}>
      <MyComponent />
    </ErrorBoundary>
  );
}

react-error-boundary 라이브러리

react-error-boundary 패키지를 사용하면 함수형 컴포넌트 스타일로 더 간편하게 Error Boundary를 구현할 수 있습니다. 재시도, 에러 리셋, fallback 커스터마이징 등 다양한 기능을 제공합니다.

tsx
import { ErrorBoundary, FallbackProps } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
  return (
    <div className="p-4 bg-red-50 rounded">
      <h2>오류가 발생했습니다</h2>
      <pre className="text-sm text-red-600">{error.message}</pre>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 에러 복구 시 실행할 로직 (상태 초기화 등)
      }}
      onError={(error, info) => {
        // 에러 리포팅 서비스로 전송
        console.error('Logged error:', error, info);
      }}
    >
      <Dashboard />
    </ErrorBoundary>
  );
}

React.memo로 리렌더링 방지

React.memo는 컴포넌트의 props가 변경되지 않으면 리렌더링을 건너뛰는 고차 컴포넌트(HOC)입니다. 렌더링 비용이 큰 컴포넌트에 적용하면 성능을 크게 개선할 수 있습니다.

tsx
import { memo, useState, useCallback } from 'react';

// memo로 감싸면 props가 같을 때 리렌더링 건너뜀
const ExpensiveList = memo(function ExpensiveList({
  items,
  onSelect,
}: {
  items: string[];
  onSelect: (item: string) => void;
}) {
  console.log('ExpensiveList 렌더링');
  return (
    <ul>
      {items.map(item => (
        <li key={item} onClick={() => onSelect(item)}>
          {item}
        </li>
      ))}
    </ul>
  );
});

function Parent() {
  const [query, setQuery] = useState('');
  const [items] = useState(['React', 'Vue', 'Angular', 'Svelte']);

  // useCallback으로 함수 참조 안정화 — memo와 함께 사용
  const handleSelect = useCallback((item: string) => {
    console.log('선택:', item);
  }, []);

  return (
    <div>
      {/* query가 변경되어도 ExpensiveList는 리렌더링되지 않음 */}
      <input value={query} onChange={e => setQuery(e.target.value)} placeholder="검색..." />
      <ExpensiveList items={items} onSelect={handleSelect} />
    </div>
  );
}

React.lazy와 Suspense

React.lazy를 사용하면 컴포넌트를 동적으로 import하여 코드 스플리팅을 적용할 수 있습니다. Suspense와 함께 사용하면 로딩 중 대체 UI를 표시합니다.

tsx
import { lazy, Suspense } from 'react';

// 동적 import — 해당 컴포넌트가 필요할 때만 로드
const HeavyChart = lazy(() => import('./components/HeavyChart'));
const AdminPanel = lazy(() => import('./components/AdminPanel'));

function App() {
  const [showChart, setShowChart] = useState(false);

  return (
    <div>
      <h1>대시보드</h1>

      <button onClick={() => setShowChart(true)}>차트 보기</button>

      {/* 로딩 중 fallback UI 표시 */}
      <Suspense fallback={<div className="animate-pulse p-4 bg-gray-100 rounded">로딩 중...</div>}>
        {showChart && <HeavyChart />}
      </Suspense>

      {/* 여러 lazy 컴포넌트를 하나의 Suspense로 감쌀 수 있음 */}
      <Suspense fallback={<p>관리 패널 로딩 중...</p>}>
        <AdminPanel />
      </Suspense>
    </div>
  );
}

리스트와 key 최적화

React에서 리스트를 렌더링할 때 key prop은 각 요소의 고유 식별자입니다. 올바른 key를 사용해야 React가 변경된 요소만 효율적으로 업데이트할 수 있습니다.

  • 고유한 ID 사용 — 배열 인덱스 대신 데이터의 고유 ID를 key로 사용
  • 인덱스 key 지양 — 순서가 변경되면 불필요한 리렌더링과 버그 발생
  • key 변경 = 재마운트 — key를 의도적으로 변경하면 컴포넌트를 완전히 재생성
tsx
// ✅ 올바른 예: 고유 ID를 key로 사용
{users.map(user => (
  <UserCard key={user.id} user={user} />
))}

// ❌ 잘못된 예: 인덱스를 key로 사용 (순서 변경 시 문제)
{users.map((user, index) => (
  <UserCard key={index} user={user} />
))}

// 💡 key 변경으로 컴포넌트 강제 재마운트
// userId가 바뀌면 Profile이 완전히 새로 생성됨
<Profile key={userId} userId={userId} />
💡

성능 최적화는 실제 성능 문제가 관측될 때 적용하세요. React DevTools의 Profiler를 사용하면 어떤 컴포넌트가 불필요하게 리렌더링되는지 확인할 수 있습니다. 조기 최적화는 코드 복잡도만 높입니다.