기본적인 리액트 웹앱에서 렌더링시 에러가 발생한 경우, 리액트는 에러가 발생한 UI를 스크린에서 제거한다. JS에서 에러는 호출자(caller) 방향으로 전파된다. 즉 콜 스택의 아래 방향(실행 중인 실행 컨텍스트가 푸시되기 직전에 푸시된 실행 컨텍스트 방향)으로 전파되기 때문에, 리액트에서도 마찬가지로 하위 컴포넌트에서 에러가 발생하면 에러가 상위 컴포넌트까지 전달되고, 에러 바운더리가 없는 경우에는 사실상 앱 전체가 스크린에서 제거되면서 흰 화면이 나타나게 된다.
이 때 에러 바운더리를 사용하면 상위에 전파되는 에러를 적절하게 끊을 수 있고 앱 전체가 보이지 않게 되는 불상사를 막을 수 있다.
ErrorBoundary 란?
React에서 'ErrorBoundary' 는 컴포넌트 하위 트리 내에서 발생할 수 있는 JavaScript 에러를 캐치하고, 이를 로깅하거나 대체 UI(fallback UI)를 보여주는 등의 대응을 할 수 있는 React 컴포넌트다. 이 기능은 앱 내에서 에러가 발생했을 때, 에러를 처리하고 애플리케이션의 나머지 부분이 정상적으로 작동할 수 있도록 함으로써 애플리케이션의 견고성을 향상시키는데 중요한 역할을 한다.
왜 'ErrorBoundary' 를 사용하나요?
React에서 에러가 발생할 경우, 발생한 에러를 잡지 못하고 최상위 앱까지 못하는 상황엔 당연히 애플리케이션 동작에 심각한 영향을 미칠 수 있다. 최상위 컴포넌트까지 전파될 경우 앱이 Crash 되어서 그대로 멈추게 될 수 있고, 데이터를 처리하는 컴포넌트 등에서 에러가 발생한다면, 데이터가 손실되거나 잘못된 데이터가 표시 될 수 있다. 이를 방지하고 사용자 경험을 향상시키기 위해 적절한 수준으로 에러 바운더리를 사용하면 다음과 같은 효과를 얻을 수 있다:
1. 안정성 향상
하위 컴포넌트에서 발생하는 에러로부터 애플리케이션을 보호하여, 예기치 못한 에러가 전체 애플리케이션을 중단시키는 것을 방지한다.
2. 에러 관리
에러 발생시 사용자에게 친숙한 메시지나 대체 페이지를 보여주어 좋은 사용자 경험(UX)을 유지할 수 있다.
3. 에러 로깅
에러 정보를 캐치하여 로깅 시스템에 전송함으로써, 개발자가 문제를 진단하고 수정하는 데 도움을 줄 수 있다.
어떻게 'ErrorBoundary' 가 작동하나요?
에러 바운더리는 React에서 현재 클래스 컴포넌트로만 구현될 수 있으며, 주로 아래 두 가지 생명주기 메소드를 사용한다.
'static getDerivedStateFromError' (직역하면, '에러로부터 파생된 상태' 를 처리하는 메소드)
- 이 메소드는 하위 컴포넌트에서 에러가 발생했을 때 호출된다.
- 오류가 발생한 이후 다음 렌더링이 일어나기 전에 호출되어 컴포넌트의 상태를 업데이트 할 수 있게 한다.
- 이 메서드는 오류 정보를 기반으로 새 상태를 반환할 수 있으므로, 대체 UI를 표시하는 등의 처리를 할 수 있다.
'componentDidCatch(error, errorInfo)'
- 에러 바운더리에서 선택적으로 사용할 수 있다. (= 즉, 사용하지 않아도 된다.)
- 이 메소드는 'getDerivedStateFromError' 메소드 다음에 호출되며, 실제로 UI가 브라우저에 렌더링 된 후 실행된다.
- 에러가 캐치된 후에 에러 로깅, 리포팅 등의 추가 로직을 위해 사용될 수 있다.
- 'errorInfo' 파라미터는 오류에 대한 추가적인 정보를 포함하며, 어떤 컴포넌트에서 오류가 발생했는지 등의 스택 트레이스를 포함한다.
아래는 React 공식 문서에서 가져온 예시 에러 바운더리이다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error, info) {
// Example "componentStack":
// in ComponentThatThrows (created by App)
// in ErrorBoundary (created by App)
// in div (created by App)
// in App
logErrorToMyService(error, info.componentStack);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return this.props.fallback;
}
return this.props.children;
}
}
이렇게 작성된 <ErrorBoundary> 컴포넌트는 다음과 같이 컴포넌트를 감싸고 대체 UI를 제공하는 방식으로 사용된다.
<ErrorBoundary fallback={<p>Something went wrong</p>}>
<SomeErrorThrownComponent />
</ErrorBoundary>
왜 함수 형태의 에러 바운더리는 없나요? ▼
React에서 에러 바운더리가 함수 형태가 아닌 클래스 형태로만 제공되는 이유는 에러 바운더리가 활용하는 라이프사이클 메서드인 'getDerivedStateFromError()' , 'componentDidCatch()' 가 함수 컴포넌트에서는 지원되지 않기 때문이라고 볼 수 있다. 이 두 메소드가 에러를 처리하고 컴포넌트의 상태를 업데이트하는 데 필수적이기 때문이다.
또한, 함수 컴포넌트는 상태와 라이프사이클 메소드를 직접적으로 가질 수 없다. 대신 Hooks API를 사용하여 상태 관리와 생명주기 이벤트를 관리한다. 리액트 팀에서 'useErrorBoundary' 와 같은 훅을 제공하지 않는 이유는 에러 처리 로직이 함수 컴포넌트의 재렌더링 사이클과 잘 맞지 않기 때문일 것이라 추측한다. 에러 처리는 에러의 영향 범위, 복구 작업 등의 이유로 전역 상태 변화를 필요로 하고, 이를 잘 관리하기 위해서는 클래스 컴포넌트를 이용한 라이프사이클 메소드가 더 적합할 수 있다.
에러 바운더리는 렌더링 컴포넌트에 대한 catch 블럭이라 볼 수 있다. catch 블럭에서 잡을 수 없는 에러가 있듯이 마찬가지로 에러 바운더리에서도 잡을 수 있는 그리고 잡을 수 없는 에러가 있고 그 종류가 상당히 유사하다.
언제 'ErrorBoundary' 가 발동되나요?
에러 바운더리는 아래와 같은 상황에서 에러를 캐치한다.
- 렌더링 도중 발생하는 에러
- 생명주기 메소드 실행 중 발생하는 에러 ---> 보통 클래스 컴포넌트에서
- 컴포넌트의 생성자(constructor) 내부에서 발생하는 에러 ---> 보통 클래스 컴포넌트에서
정리하자면, React 의 에러 바운더리는 컴포넌트의 렌더링 또는 생명주기 메소드 중에 동기적으로 발생하는 에러에 대해서만 대응할 수 있다.
Error Boundary 에서 잡을 수 없는 에러
1. 이벤트 핸들러 내부에서 발생하는 에러
이벤트 핸들러는 직접적으로 렌더링 과정과 연결되어 있지 않기 때문이다. 이 대문에 에러 처리를 위해선 직접적으로 에러 처리 로직을 이벤트 핸들러 내부에 구현해야 한다.
아래의 컴포넌트는 직접적으로, 그리고 동기적으로 에러를 던지고 있지만, 이벤트 핸들러 내부에서 발생하기 때문에 에러를 잡지 못한다. 여기서 발생하는 에러를 처리하기 위해선 해당 핸들러 내부에 try/catch 블록을 사용해야 한다.
function EventhandlerTestComponent() {
const handleClick = () => {
throw new Error("Click Error!");
};
return <button onClick={handleClick}>Test Button</button>;
}
// 사용시: 에러를 잡지 못하고 fallback UI가 나타나지 않는다!
<ErrorBoundary fallback={<div>Something went wrong! in a Button compoent.</div>} >
<EventhandlerTestComponent />
</ErrorBoundary>
2. 비동기 코드에서 발생하는 에러
에러 바운더리는 동기적으로 발생하는 에러만 잡을 수 있다. 따라서 'setTimout', 'setInterval', 그리고 Promise 와 같은 비동기 API 호출 등 비동기 함수 내에서 발생하는 에러는 에러 바운더리에서 잡을 수 없다.
따라서 아래의 코드는 ErrorBoundary에서 잡을 수 없다.
function AsyncTestComponent() {
const [data, setData] = useState();
useEffect(() => {
async function fetchData() {
const response = await Promise.reject("Hello World!");
setData(response);
}
fetchData(); // useQuery 등을 이용할 때도 마찬가지!
}, []);
return <div>{data}</div>;
}
// JS에서 Promise.reject() 는 즉시 거부되는 프로미스를 생성하며 에러로 간주된다.
// 동기 코드에서 예외를 던지는 것과 비슷한 방식이다.
// fallback UI가 보이지 않는다!
<ErrorBoundary fallback={<div>Async Failed!</div>} >
<AsyncTestComponent />
</ErrorBoundary>
위 예시에서 발생한 에러를 처리하기 위해선 컴포넌트를 아래와 같은 방식으로 변경해야 한다.
여기에선 비동기 함수에서 try/catch 와 에러 상태를 저장하는 방식으로 컴포넌트 자체적으로 에러를 처리하고 있다.
export default function AsyncTestComponent() {
const [data, setData] = useState();
const [error, setError] = useState();
useEffect(() => {
async function fetchData() {
try {
const response = await Promise.reject("Hello World!");
console.log("response", response);
setData(response);
} catch (error) {
// 여기서 throw error; 하더라도 에러 바운더리에서 잡히지 않는다!
// throw new Error('Async Error'); 또한 잡히지 않는다!!
console.error("Error fetching data", error);
setError(error);
}
}
fetchData();
}, []);
if (error) {
return <div>Error: {error}</div>; // fallback UI
}
return <div>{data}</div>;
}
3. 서버 사이드 렌더링 중 발생하는 에러
에러 바운더리는 클라이언트 사이드의 렌더링 트리에서 발생하는 에러만을 처리한다. 서버 사이드 렌더링(SSR)에서 발생하는 에러는 에러바운더리에서 잡을 수 없다. SSR은 클라이언트 사이드 렌더링과 다르게 서버에서 HTML을 생성하고, 이 과정에서 발생하는 에러는 서버 측의 에러 핸들링 로직을 통해 처리해야 한다.
덧붙이자면, 서버에서 발생하는 에러는 보안이나 안정성과 관련이 있을 수 있기 때문에, 에러 메시지를 직접 사용자에게 보여주기 보다는 로깅하고, 일반적인 오류 메시지를 클라이언트에 던지는 것이 좋다. 또한, 서버 사이드 렌더링은 CSR 보다 리소스와 성능에 민감할 수 있으므로, 에러 처리 로직을 최적화하는 것이 중요하다.
4. Error Boundary 자체 내부에서 발생하는 에러
try/catch 블럭에서 catch 블럭 내에서 발생하는 에러를 잡을 수 없듯이 에러 바운더리 내부 코드에서 발생하는 에러는 그 에러 바운더리에서 처리할 수 없다. 이는 에러 바운더리 컴포넌트 설계의 기본 제한이라고 한다.
에러 바운더리는 React 웹앱의 견고성을 향상시키는 중요한 도구이다. 동기적 에러를 효과적으로 관리하고, 특히 렌더링 과정 중 발생할 수 있는 예외적인 상황들을 안전하게 처리하여 사용자에게 더 안정적인 인터페이스를 제공할 수 있도록 돕는다. 그러나 에러 바운더리는 모든 유형의 에러를 처리할 수 있는 'Silver bullet'은 아니다. 이벤트 핸들러나 비동기 작업과 같이 에러 바운더리의 범위 밖에서 발생하는 에러는 추가적인 에러 핸들링이 필요하다. 따라서 프론트엔드 개발자는 Error Boundary를 사용하여 대부분의 UI 에러를 포착하고 try/catch 등을 이용하여 잡을 수 없는 나머지 에러를 처리함으로써 애플리케이션의 에러 관리 체계를 강화해야 한다.
Handle Errors Gracefully With Error Boundaries in Reactjs - https://upmostly.com/tutorials/handle-errors-gracefully-with-error-boundaries-in-reactjs
In-Depth Tutorial for Mastering React Error Boundaries - https://reliasoftware.com/blog/react-error-boundary
Error Boundaries in React: A Comprehensive Guide - https://zenn.dev/samsmithhh1/articles/c453dff65abac7#try...catch
리액트 공식 문서(legacy) - https://legacy.reactjs.org/docs/error-boundaries.html
리액트 공식 문서 - https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
'React' 카테고리의 다른 글
리액트 상태 관리 전략 - Context API 와 상태관리 라이브러리 (0) | 2024.08.09 |
---|---|
React - useQuery 를 사용할 때 에러가 발생한다면..? (0) | 2024.07.11 |
Suspense in React | 리액트 서스펜스 (0) | 2024.06.22 |
Why React? | 리액트 왜 사용하세요? (0) | 2024.05.14 |