React

Errorboundary 내부동작 이해하기

한우코딩 2025. 3. 23. 10:51

React ErrorBoundary 이해하기: 내부 동작 원리와 활용법

React 애플리케이션을 개발하다 보면 예기치 않은 에러를 마주치게 됩니다. 컴포넌트가 렌더링 과정에서 에러가 발생하면 어떻게 될까요? React 16에서 도입된 ErrorBoundary로 이러한 문제를 해결할 수 있습니다. 이 글에서는 ErrorBoundary의 개념부터 내부 동작 원리, 그리고 실제 활용법까지 알아보겠습니다.

ErrorBoundary란 무엇인가?

ErrorBoundary는 React 컴포넌트 트리에서 자식 컴포넌트에서 발생하는 JavaScript 에러를 캐치하고, 에러 발생 시 폴백 UI를 보여주는 컴포넌트입니다. 간단히 말하면, React 컴포넌트를 위한 선언적인 try...catch 문이라고 볼 수 있습니다.

<ErrorBoundary fallback={<p>Something went wrong</p>}>
  <Profile />
</ErrorBoundary>

위 코드에서 Profile 컴포넌트가 렌더링 중에 에러를 던지면, ErrorBoundary는 에러를 캐치하고 폴백 UI인 "Something went wrong"을 표시합니다.

ErrorBoundary 구현하기

ErrorBoundary는 클래스 컴포넌트로, static getDerivedStateFromError() 메서드나 componentDidCatch() 생명주기 메서드 중 하나 이상을 구현해야 합니다.

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

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI를 표시하기 위해 상태 업데이트
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 에러 정보를 로깅하거나 에러 보고 서비스에 보낼 수 있음
    console.error("ErrorBoundary caught an error:", error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI 렌더링
      return this.props.fallback || <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

ErrorBoundary의 내부 동작 원리

ErrorBoundary가 어떻게 작동하는지 이해하기 위해 React의 내부 구현을 살펴봅시다. 여기서 중요한 개념은 "파이버(Fiber)"라는 React의 내부 렌더링 단위입니다.

1. 에러 발생과 ErrorBoundary 검색

React 컴포넌트가 렌더링 도중 에러를 던지면, React는 다음과 같은 과정을 거칩니다

// 간소화된 React 내부 구현
function renderRootSync(root, lanes) {
  do {
    try {
      workLoopSync(); // 컴포넌트 렌더링 시도
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue); // 에러 처리
    }
  } while (true);
}

여기서 볼 수 있듯이, React는 렌더링 과정을 큰 try...catch 블록으로 감싸고 있습니다. 컴포넌트가 에러를 던지면, handleError 함수가 호출됩니다.

handleError 함수는 throwException을 호출하고, 이 함수는 파이버 트리를 거슬러 올라가며 가장 가까운 ErrorBoundary를 찾습니다:

function throwException(root, returnFiber, sourceFiber, value, lanes) {
  sourceFiber.flags |= Incomplete; // 에러가 발생한 파이버에 표시

  // 에러 값 캡처
  value = createCapturedValueAtFiber(value, sourceFiber);

  // 상위로 올라가며 ErrorBoundary 검색
  let workInProgress = returnFiber;
  do {
    switch (workInProgress.tag) {
      case ClassComponent:
        // ErrorBoundary인지 확인
        if (
          typeof workInProgress.type.getDerivedStateFromError === 'function' ||
          (workInProgress.stateNode !== null &&
           typeof workInProgress.stateNode.componentDidCatch === 'function')
        ) {
          // ErrorBoundary 찾음
          workInProgress.flags |= ShouldCapture;

          // ErrorBoundary에 업데이트 예약
          const update = createClassErrorUpdate(
            workInProgress,
            value,
            lane
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
        break;
    }
    workInProgress = workInProgress.return;
  } while (workInProgress !== null);
}

이 코드에서 중요한 점은

  1. 에러가 발생한 파이버에 Incomplete 플래그를 설정합니다.
  2. 파이버 트리를 상위로 탐색하며 getDerivedStateFromError 또는 componentDidCatch를 구현한 클래스 컴포넌트(ErrorBoundary)를 찾습니다.
  3. ErrorBoundary를 찾으면 ShouldCapture 플래그를 설정하고, 상태 업데이트를 예약합니다.

2. "Unwinding" 과정과 DidCapture 플래그

React는 에러가 발생한 파이버에서부터 작업을 완료해야 합니다. 이 과정을 "unwinding"이라고 하며, 리액트 내부의completeUnitOfWork 함수에서 이루어집니다

function completeUnitOfWork(unitOfWork) {
  let completedWork = unitOfWork;
  do {
    if ((completedWork.flags & Incomplete) === NoFlags) {
      // 정상적인 완료 과정
    } else {
      // 에러로 인해 완료되지 않은 파이버
      const next = unwindWork(completedWork);

      if (next !== null) {
        // 새로운 작업이 생성됨 (ErrorBoundary에서 다시 시작)
        workInProgress = next;
        return;
      }
    }
    // 부모로 이동
    completedWork = completedWork.return;
  } while (completedWork !== null);
}

unwindWork 함수에서는 ShouldCapture 플래그를 DidCapture로 변경합니다:

function unwindWork(workInProgress) {
  switch (workInProgress.tag) {
    case ClassComponent:
      const flags = workInProgress.flags;
      if (flags & ShouldCapture) {
        // ShouldCapture를 DidCapture로 변경
        workInProgress.flags = (flags & ~ShouldCapture) | DidCapture;
        return workInProgress; // 이 파이버에서 다시 시작
      }
      return null;
  }
}

이렇게 DidCapture 플래그가 설정된 ErrorBoundary는 다시 렌더링을 시도합니다.

3. ErrorBoundary의 재렌더링

ErrorBoundary 클래스 컴포넌트가 다시 렌더링될 때, React는 finishClassComponent 함수를 통해 렌더링 처리를 합니다:

function finishClassComponent(current, workInProgress, Component, didCaptureError) {
  const instance = workInProgress.stateNode;

  // didCaptureError가 true면, ErrorBoundary가 에러를 캐치한 상태
  let nextChildren;
  if (didCaptureError) {
    // 에러를 캐치했으므로, 새로운 상태로 render() 메서드 호출
    nextChildren = instance.render();
  }

  // 자식들 재조정 (reconciliation)
  reconcileChildren(current, workInProgress, nextChildren, renderLanes);

  return workInProgress.child;
}

여기서 중요한 점은

  1. ErrorBoundary가 에러를 캐치하면, 이전에 예약한 업데이트를 통해 새로운 상태로 렌더링합니다.
  2. getDerivedStateFromError에서 반환된 상태가 적용되어, ErrorBoundary는 폴백 UI를 렌더링합니다.

ErrorBoundary의 실용적 활용법

1. 선언적 오류 처리

ErrorBoundary의 가장 큰 장점은 선언적으로 오류를 처리할 수 있다는 점입니다

<ErrorBoundary
  fallback={<ErrorPage />}
  onError={(error, info) => logErrorToService(error, info)}
>
  <App />
</ErrorBoundary>

2. 세분화된 오류 경계 설정

다양한 부분에 ErrorBoundary를 배치하여 애플리케이션의 일부분만 영향받게 할 수 있습니다

function MainContent() {
  return (
    <div>
      <Header />
      <ErrorBoundary fallback={<p>콘텐츠를 로드할 수 없습니다.</p>}>
        <Content />
      </ErrorBoundary>
      <ErrorBoundary fallback={<p>사이드바 로드 실패</p>}>
        <Sidebar />
      </ErrorBoundary>
      <Footer />
    </div>
  );
}

3. 리액트 훅으로 ErrorBoundary 사용하기

함수형 컴포넌트에서도 ErrorBoundary를 쉽게 사용할 수 있도록 커스텀 훅을 만들 수 있습니다

function useErrorBoundary() {
  const [hasError, setHasError] = useState(false);
  const errorBoundaryRef = useRef(null);

  // ErrorBoundary 컴포넌트 생성
  if (!errorBoundaryRef.current) {
    errorBoundaryRef.current = class extends React.Component {
      state = { hasError: false };

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

      componentDidCatch(error, info) {
        setHasError(true);
        if (props.onError) props.onError(error, info);
      }

      render() {
        if (this.state.hasError) {
          return this.props.fallback;
        }
        return this.props.children;
      }
    };
  }

  return {
    ErrorBoundary: errorBoundaryRef.current,
    hasError
  };
}

4. 실제 프로덕션 환경에서의 ErrorBoundary

실제 프로덕션 애플리케이션에서는 ErrorBoundary를 다음과 같이 확장할 수 있습니다

class ProductionErrorBoundary extends React.Component {
  state = {
    hasError: false,
    error: null,
    errorInfo: null
  };

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

  componentDidCatch(error, errorInfo) {
    this.setState({ errorInfo });

    // 에러 모니터링 서비스에 보고
    Sentry.captureException(error, { extra: errorInfo });

    // 분석 서비스에 이벤트 기록
    trackEvent('error_boundary_triggered', {
      componentStack: errorInfo.componentStack,
      message: error.message
    });
  }

  render() {
    if (this.state.hasError) {
      return (
        <ErrorPage
          error={this.state.error}
          componentStack={this.state.errorInfo?.componentStack}
          onReset={() => this.setState({ hasError: false, error: null, errorInfo: null })}
        />
      );
    }

    return this.props.children;
  }
}

ErrorBoundary의 한계

ErrorBoundary는 강력하지만, 몇 가지 한계가 있습니다:

  1. 이벤트 핸들러 내부의 에러를 캐치하지 않음: ErrorBoundary는 렌더링 과정에서 발생하는 에러만 캐치합니다. 이벤트 핸들러 내부의 에러는 일반 try...catch로 처리해야 합니다.

  2. 비동기 코드에 대한 에러 처리: Promise 거부나 비동기 함수에서 발생하는 에러도 ErrorBoundary로 캐치할 수 없습니다.

  3. 서버 사이드 렌더링: 서버 사이드 렌더링에서는 다르게 동작할 수 있습니다.

  4. 오직 클래스 컴포넌트로만 구현 가능: 현재 React는 함수형 컴포넌트에서 ErrorBoundary 기능을 제공하지 않습니다.

결론

ErrorBoundary는 React 애플리케이션의 안정성을 높이는 필수적인 도구입니다. 내부적으로는 복잡한 메커니즘으로 동작하지만, 사용자 입장에서는 매우 간단하고 명확한 API를 제공합니다.

오류 처리는 좋은 사용자 경험을 위한 중요한 부분입니다. ErrorBoundary를 효과적으로 활용하면 예기치 않은 에러에도 우아하게 대응하는 견고한 React 애플리케이션을 만들 수 있습니다.