웹 앱을 구축하는 것은 복잡한 과정이다. 때때로 문제가 발생하게 되며, 이런 문제를 완전히 막을 수는 없다. 예상치 못한 방식으로 오류가 발생할 것을 예상
하고 그에 대응할 준비
가 되어 있어야 한다.
이를 위한 Error Boundary를 알아보고 직접 만들어 보자.
에러에 대한 경계
를 의미하는 Error Boundary는 경계 내의 구간에서 에러가 발생하면 그 에러를 잡아내서 처리할 수 있는 역할을 한다. 기본적인 로직은 아래와 같다.
준비
가 따로 되어 있지 않다면 React에서 제공하는 에러페이지로 넘어간다.
- 실제 환경에서는 아무 페이지도 뜨지 않을 것이다. 사용자는 사라진다..😢
- 예상치 못한 에러가 발생하면 사용자에게 다시 홈부터 시작하도록 유도할 수 있다.
공식문서를 살펴보면 Error Boundary는 현재 함수형 컴포넌트로 작성할 수 있는 방법이 없으며 클래스로 예시 코드를 알려주고 있다.
클래스를 직접 작성하기 싫다면 react-error-boundary를 제공해 준다.
클래스 예시 코드를 참고하여 코드를 생성해보자.
import { Component, ErrorInfo, ReactNode } from 'react';
// 에러바운더리를 사용할 때 fallback prop을 제공하지 않으면 사용되는 기본 UI
import { DefaultErrorFallback } from './DefaultErrorFallback';
// 에러바운더리 컴포넌트 props 타입 정의
interface ErrorBoundaryProps {
children: ReactNode; // 렌더링될 자식 컴포넌트들
fallback?: ReactNode; // 에러 발생시 보여주는 컴포넌트
}
// 에러바운더리 컴포넌트 state 타입 정의
interface ErrorBoundaryState {
hasError: boolean; // 에러 발생 여부
error?: Error; // 에러 객체
}
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
// 생성자에서 초기 상태 설정
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: undefined };
}
// React의 생명주기 메서드 'getDerivedStateFromError'
// 하위 컴포넌트에서 에러가 발생하면 호출된다.
// 에러 객체를 받아서 새로운 상태를 반환한다.
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
// React의 생명주기 메서드 'componentDidCatch'
// 에러와 에러 정보를 받아서 처리한다.
// 분석 서비스에 에러를 기록하는 등 추가 작업이 가능하다.
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error(error, errorInfo);
}
// 컴포넌트 렌더링
render() {
// 에러가 있으면 fallback 렌더링
if (this.state.hasError && this.state.error) {
return (
this.props.fallback || <DefaultErrorFallback error={this.state.error} />
);
}
// 에러 없으면 children 렌더링
return this.props.children;
}
}
// Layout
import { Outlet } from 'react-router-dom';
import { ErrorBoundary } from '@/components/ErrorBoundary';
export const Layout = () => {
return (
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
);
};
// Router
import { createBrowserRouter } from 'react-router-dom';
import { Home } from '@/routes/Home';
import { Detail } from '@/routes/Detail';
import { Layout } from '@/routes/Layout';
import { ROUTE_PATH } from '@/router/routePath';
import { NotFound } from '@/routes/NotFound';
import { ErrorBoundary } from '@/components/ErrorBoundary';
export const router = createBrowserRouter([
{
element: <Layout />,
path: ROUTE_PATH.ROOT,
errorElement: <NotFound />,
children: [
{
path: ROUTE_PATH.HOME,
element: <Home />,
},
{
path: ROUTE_PATH.DETAIL,
element: (
<ErrorBoundary>
<Detail />
</ErrorBoundary>
),
},
],
},
]);
Detail
라우트에서만 발생하는 오류를 ErrorBoundary
로 캡처한다. Home
라우트에서 발생하는 오류는 캡처되지 않는다.// Component
import { ErrorBoundary } from '@/components/ErrorBoundary';
const Detail = () => {
return (
<div>
<h1>Detail Page</h1>
<ErrorBoundary>
<Profile />
</ErrorBoundary>
</div>
);
}
Profile
컴포넌트 내에서 발생하는 오류만 ErrorBoundary
로 캡처된다.export const Detail = () => {
// 중략
// 에러 바운더리 확인을 위한 에러 유도 코드
useEffect(() => {
async function test() {
throw new Error('에러바운더리 테스트');
}
test();
}, []);
return (
// 중략
)
Error Boundary는 컴포넌트 트리 내에서 하위 컴포넌트의 렌더링에서 발생
하는 에러를 잡는다.
즉, 아래의 경우에는 에러를 포착할 수 없다.
이벤트 핸들러
Error Boundary는 이벤트 핸들러 내에서 발생하는 오류를 포착하지 않는다. 예를 들어, onClick
또는 onChange
와 같은 이벤트 핸들러에서 발생하는 오류는 Error Boundary로 잡히지 않는다.
비동기 코드
setTimeout
, requestAnimationFrame
, 서버 사이드 렌더링
등 비동기 코드에서 발생하는 오류는 Error Boundary에서 포착되지 않는다.
Server Side Rendering (SSR)
서버 사이드 렌더링 중
에 발생하는 오류는 Error Boundary에서 잡히지 않는다.
Error Boundary 자체의 오류
Error Boundary 컴포넌트 내부에서 발생하는 오류는 해당 Error Boundary에서 포착되지 않는다. 이러한 오류를 포착하려면 상위 컴포넌트에서 다른 Error Boundary를 사용해야 한다.
외부 라이브러리와 코드
Error Boundary는 React 컴포넌트 트리 내에서 발생하는 오류만 포착한다. 따라서 React 외부에서 발생하는 오류나 외부 라이브러리에서 발생하는 오류는 포착되지 않는다.
위 내용들처럼 Error Boundary는 에러에 대한 준비를 가능하게 해줌으로서 애플리케이션의 안정성을 향상 시켜주지만, 한계점이 분명하기에 전체 오류 관리 전략의 일부
로서 사용되도록 해야한다.
오류 경계를 너무 적게 사용하는 경우
애플리케이션의 상단에 단 하나의 오류 경계만 있으면, 한 부분에서의 오류가 전체 애플리케이션에 영향을 미칠 수 있다.
오류 경계를 과도하게 사용하는 경우
각 컴포넌트마다 오류 경계를 설정하면, 사용자 경험이 혼란스러워질 수 있다. 또한 성능에도 영향을 미칠 수 있다.
애플리케이션의 기능 경계
를 확인하고 그곳에 오류 경계를 배치하는 것이 가장 좋다.
Twitter를 예로 들면, 메인 콘텐츠 섹션은 Home
, Trends for you
, Who to follow
등 세 가지로 구분된다.
Who to Follow 섹션 내에서도 여러 하위 섹션이 있으며, 각각의 섹션에서 오류가 발생할 경우 다른 섹션에 영향을 주어야 하는지를 고려하여 오류 경계를 설정한다.
애플리케이션의 오류 허용성을 테스트하는 가장 좋은 방법은 일부러 오류를 발생시켜보는 것이다.
function CreditCardInput(props) {
// What happens if I messed up here? Let's find out!
throw new Error("oops, I made a mistake!")
return <input className="credit-card" />
}