들어가기
React 애플리케이션에서 예기치 못한 에러는 애플리케이션의 안정성을 해치고 사용자 경험을 저하시킬 수 있습니다. 이러한 에러들을 효과적으로 처리하고 관리하기 위해 React는 ErrorBoundary라는 컴포넌트를 사용할 수 있습니다.
Error Boundary란 에러에 대한 경계를 의미합니다. 즉, 특정 `Error Boundary`로 감싸여진 구간에서 에러가 발생하면 그 에러를 잡아내서 처리할 수 있습니다.
선언적으로 에러를 처리할 수 있는 ErrorBoundary 컴포넌트는 크게 두 가지 방식으로 구현할 수 있습니다
- Class Component로 선언해서 사용하는 법
- 라이브러리를 통해 Function Component로 사용하는 법
1. Class Component로 ErrorBoundary 구현하기
ErrorBoundary는 React 컴포넌트의 생명주기 메서드를 활용하여 에러를 캐치합니다. 주로 사용되는 두 가지 메서드는 다음과 같습니다
- getDerivedStateFromError(error): 에러 발생 시 호출되어 컴포넌트의 state를 업데이트합니다.
- componentDidCatch(error, info): 에러가 발생한 후 실행되며, 에러 로깅 등의 추가적인 효과를 수행합니다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false, error: null };
}
static getDerivedStateFromError(error) {
// 에러 발생 시 호출
return { hasError: true, error };
}
componentDidCatch(error, info) {
// 에러 로깅
console.log("Error caught by ErrorBoundary: ", error);
console.log("Error details: ", info.componentStack);
}
render() {
if (this.state.hasError) {
// Fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
동작설명
위 코드에서 ErrorBoundary 컴포넌트는 다음 단계를 통해 에러를 처리합니다:
- 에러 감지: 자식 컴포넌트에서 발생한 에러는 `getDerivedStateFromError`를 통해 감지됩니다.
- 상태 업데이트: 에러 정보를 상태에 저장하여 렌더링 트리를 업데이트합니다.
- 에러 처리: `componentDidCatch` 메서드를 통해 추가적인 에러 처리(e.g. 로깅)를 수행합니다.
- Fallback UI 렌더링: 에러 발생 시 사용자에게 표시될 UI를 제공합니다.
3. Function Component에서 ErrorBoundary 사용하기
함수 컴포넌트는 생명주기 메서드를 직접 사용할 수 없기 때문에, `react-error-boundary` 라이브러리를 사용하여 에러 바운더리를 적용할 수 있습니다.
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({error, resetErrorBoundary}) {
return (
<div role="alert">
<p>Something went wrong:</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>Try again</button>
</div>
);
}
const MyComponent = () => {
return (
<ErrorBoundary
FallbackComponent={ErrorFallback}
onReset={() => {
// reset the state of your app so the error doesn't happen again
}}
>
<ComponentThatMayThrowError />
</ErrorBoundary>
);
}
동작설명
위의 함수 컴포넌트 예시에서 ErrorBoundary는 에러가 발생했을 때 ErrorFallback 컴포넌트를 렌더링합니다. 사용자는 'Try again' 버튼을 클릭하여 에러 바운더리를 리셋할 수 있습니다.
이 라이브러리에서 ErrorBoundary props으로 제공하는`FallbackComponent`, `onReset`, `onError` 등을 이용하여 더 간단하게 에러바운더리 설정을 할 수 있습니다.
각각 클래스 컴포넌트 방식과는 이렇게 대응될 수 있습니다.
`FallbackComponent` === 클래스 에러바운더리의 `fallback prop`
`onReset` === `reset()` 메서드: 라이브러리 내부적으로 error 상태를 초기화용
`onError` === `componentDidCatch`메서드: 로깅용
또한 클래스에서는 `getDerivedStateFromError` 에서 직접 에러 상태를 바꿔서 `fallback UI`를 렌더링 해야 했지만, 라이브러리에서는 내부적으로 해주기 때문에 구현할 필요가 없어 더욱 간단해집니다.
4. react-query와 errorBoundary를 함께 사용할 때
react-query 라이브러리를 사용하면, 에러 처리를 더 효율적으로 관리할 수 있습니다. `throwOnError` 옵션과 `QueryErrorResetBoundary`를 사용하여 에러를 관리할 수 있습니다.
import { ErrorBoundary } from 'react-error-boundary';
import ErrorFallback from './ErrorFallback';
import ErrorableComponent from './ErrorableComponent';
import { ErrorInfo } from 'react';
import { QueryErrorResetBoundary } from '@tanstack/react-query';
const Example = () => {
...
return (
<QueryErrorResetBoundary>
// queryCache reset하기
{({ reset }) => (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={reset} onError={logError}>
<ErrorableComponent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
);
};
export default Example;
`QueryErrorResetBoundary`의 경우 에러가 발생했을 때 적절한 UI를 렌더링하고, 필요한 경우 캐시를 초기화하는 등의 처리를 합니다.
5. Error Boundary의 한계
Error Boundary는 많은 JavaScript 에러를 효과적으로 처리하지만, 모든 상황에서 에러를 잡아낼 수 있는 것은 아닙니다. 특히, React의 useEffect 훅 내에서 발생한 에러는 Error Boundary로 잡을 수 없습니다.
import { useEffect } from 'react';
function UnstableComponent() {
useEffect(() => {
// useEffect 내부에서 발생한 에러
throw new Error("Failed to fetch data!");
}, []);
return <div>Unstable Component</div>;
}
위 코드에서 `UnstableComponent`는 useEffect 내에서 에러를 발생시키고 에러바운더리로 던지고 있습니다.
하지만 이 에러는 Error Boundary에서 잡히지 않습니다.
useEffect에서의 에러 처리
`useEffect`는 React 컴포넌트가 렌더링된 후에 실행되는 사이드 이펙트(logic)를 처리하기 위한 훅입니다. 이 훅 내부에서 발생하는 에러는 렌더링 과정과 별개로 처리되기 때문에, Error Boundary에서 잡히지 않습니다.
왜 useEffect에서 발생한 에러는 잡히지 않을까?
React의 렌더링 생명주기와 관련된 에러만을 Error Boundary가 잡을 수 있습니다. 즉, 컴포넌트가 렌더링 과정에서 발생한 에러만을 처리할 수 있습니다. 하지만 useEffect 내부에서 실행되는 코드는 비동기적으로 처리되며, 컴포넌트의 렌더링과 독립적입니다. 따라서 이러한 에러는 Error Boundary의 범위 밖에 있습니다.
이러한 한계를 극복하기 위해, useEffect에서 발생할 수 있는 에러를 관리하는 다른 방법을 사용해야 합니다.
주로 비동기 작업을 처리하는 경우에는 다음과 같은 방법을 고려할 수 있습니다.
- 에러 핸들링: `try...catch` 블록을 사용하여 비동기 작업 내에서 직접 에러를 핸들링합니다.
- 상태 관리: 에러 발생 시, 해당 에러를 상태에 저장하고 이를 컴포넌트에서 조건적으로 렌더링하여 사용자에게 에러를 알립니다.
import { useEffect, useState } from 'react';
function SafeComponent() {
const [error, setError] = useState(null);
useEffect(() => {
try {
// 예를 들어 비동기 요청 로직
throw new Error("Failed to fetch data!");
} catch (err) {
setError(err);
}
}, []);
if (error) {
return <div>Error occurred: {error.message}</div>;
}
return <div>Safe Component</div>;
}
위 예시에서는 useEffect 내부에서 발생한 에러를 catch 구문을 통해 잡고, 이를 상태로 저장하여 조건부 렌더링으로 사용자에게 에러 정보를 제공하고 있습니다.
6. 에러바운더리가 잡지 않는 에러
ErrorBoundary는 다음과 같은 에러를 잡지 않습니다:
- 이벤트 핸들러에서 발생하는 에러
- 비동기 코드에서 발생하는 에러 (예: setTimeout, promise)
- 서버 사이드 렌더링에서 발생하는 에러
- 자식이 아닌 에러 경계 자체에서 발생하는 에러
Reference
https://ko.legacy.reactjs.org/docs/error-boundaries.html#introducing-error-boundaries