Skip to content

ErrorBoundary 학습 정리

SeongYeon edited this page Jan 17, 2025 · 1 revision

기존 에러 처리 방식의 한계

프로젝트 일정 상 기능 개발에 치중하다 보니 에러 처리에 대한 깊은 고민을 해본적이 없다.

기존 에러 처리 방식은 api 호출 시 에러가 발생하면 컴포넌트 내부에서 조건부 렌더링을 통해 그에 맞는 fallback ui를 보여주는 방식이였다.

하지만 위 개발 방식에서 api를 호출하는 모든 컴포넌트에서 Error 처리 시 불편함을 느꼈다. 기존에 에러를 처리하는 방식은 다음과 같았다.

아래 코드는 기존 에러 처리 로직을 설명하기 위한 매우 간단한 코드로 내부의 자세한 로직은 생략한다. 개발 환경은 React 와 Tanstack Query를 사용했다.

function ExampleComponent() {
    const { data, error } = useSuspenseQuery({
        queryKey: ['abc'],
        queryFn: fetchUserData
    });

    if (error) {
        return <div>{error.message} 에러 발생...!</div>;
    }

    return (
        <div>
            <h1>{data.name}님의 프로필</h1>
            <div>{data.email}</div>
        </div>
    );
}

프로젝트를 진행하면 할 수록 위 에러 처리 방식의 다음과 같은 문제점을 팀원 모두 느낄 수 있었다

  1. 중복 코드 발생: API를 호출하는 모든 컴포넌트에서 동일한 에러 처리 로직을 반복적으로 작성해야 했다.
  2. 일관성 없는 에러 UI: 각 컴포넌트 마다 서로 다른 방식으로 에러를 표시하며, 통일된 에러 컴포넌트가 없어, 사용자 경험이 일관적이지 않았다
  3. 유지보수의 어려움: 에러 처리 방식을 변경하려면 모든 컴포넌트를 수정해야 했다.

이러한 문제들로 인해 더 나은 에러 처리 방식의 필요성을 느끼게 되었다. 특히 API 호출이 많은 우리 프로젝트에서 각 컴포넌트마다 반복되는 에러 처리 로직은 개발 생산성을 저해하는 주요 요인이었다. 이에 조금 더 중앙화된 에러 처리 방식을 고민하던 중 React의 Error Boundary를 알게 되었고, 이를 프로젝트에 적용하기로 결정했다.

Error Boundary

💡

Error Boundary는 React16에서 도입된 기능으로, 하위 컴포넌트 트리에서 발생하는 JavaScript 에러를 감지하고, 에러 발생 시 fallback UI를 보여줄 수 있는 React 컴포넌트이다.

클래스 컴포넌트에서만 구현할 수 있으며, getDerivedStateFromError()componentDidCatch() 생명주기 메서드등을 활용하여 구현한다.

Error Boundary 구현

내가 구현한 APIErrorBoundary를 살펴보자.

import React, { Component, ErrorInfo, ReactNode } from 'react';
interface Props {
	children: ReactNode;
	fallback: React.ComponentType<{ error: Error }>;
}

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

export class ApiErrorBoundary 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 an error:', error, errorInfo);
	}

	render() {
		const { hasError, error } = this.state;
		const { children, fallback: Fallback } = this.props;

		if (hasError && error) {
			return <Fallback error={error} />;
		}

		return children;
	}
}

export const DefaultErrorFallback = ({ error }: { error: Error }) => {
	return (
		<div className="p-4 bg-red-50 border border-red-200 rounded-lg">
			<h2 className="text-lg font-semibold text-red-800">
				에러가 발생했습니다
			</h2>
			<p className="mt-2 text-sm text-red-600">{error.message}</p>
		</div>
	);
};

내가 만든 Error boundary에서 주목할 만한 점은 fallbackUI를 prop으로 받아 각 컴포넌트마다 자체 에러 UI를 유연하게 처리할 수 있도록 구현한 것이다. 이를 통해 상황과 각 페이지에 맞는 적절한 에러 UI를 제공할 수 있다. 이렇게 구현한 ApiErrorBoundary는 다음과 같이 사용할 수 있다.

<ApiErrorBoundary fallback={DefaultErrorFallback}>
    <MyComponent />
</ApiErrorBoundary>

// 커스텀 에러 UI 사용
<ApiErrorBoundary 
    fallback={({ error }) => (
        <div>커스텀 에러 메시지: {error.message}</div>
    )}
>
    <MyComponent />
</ApiErrorBoundary>

Error Boundary 도입 후 느낀 장점

Error Boundary를 적용하고 실제 개발 과정에서 체감한 장점들을 다음과 같다

선언적 에러 처리

<ApiErrorBoundary fallback={DefaultErrorFallback}>
    <MyComponent />
</ApiErrorBoundary>

Error Boundary를 도입하며 가장 크게 체감한 장점은 선언적인 방식으로 에러를 처리할 수 있다는 점이었다. 기존에는 각 컴포넌트에서 조건문을 통해 명령적으로 에러를 처리했지만, Error Boundary를 사용하면 컴포넌트를 감싸는 것만으로 에러 처리가 가능해졌다. 이로 인해 컴포넌트 내부는 성공 상태에만 집중할 수 있게 되었다.

에러 처리 로직 중앙화

Error Boundary를 통해 에러 처리를 중앙화함으로써 세 가지 큰 이점을 얻을 수 있었다:

1. 일관된 에러 처리

  • 모든 API 에러에 대해 동일한 방식으로 대응
  • 에러 메시지 형식 통일
  • 재시도 로직과 에러 로깅의 일관성 확보

2. 향상된 유지보수성

  • 에러 처리 방식 변경 시 Error Boundary 컴포넌트만 수정
  • 관심사의 명확한 분리
  • 변경 사항의 영향 범위 최소화
  1. 코드 중복 제거
  • 반복적인 에러 처리 코드 제거
  • 더욱 깔끔해진 컴포넌트 코드
  • 핵심 로직에 집중할 수 있는 환경

마무리

Error Boundary의 도입은 단순한 기능 추가를 넘어 전반적인 코드 품질 향상과 개발 생산성 증가로 이어졌다. 앞으로도 React의 다양한 기능들을 활용해 더 나은 코드를 작성하기 위해 계속 고민해볼 예정이다.