이전 글 참고) ErrorBoundary
공식문서) https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
에러를 명령형방식 => 선언형방식으로 처리할 수 있게 해주는 컴포넌트임.
보통 Suspense와 함께 사용해서 Class형 컴포넌트로 작성함.
React-Error-Boundary 라이브러리를 사용하면 => Functional Component로 ErrorBoundary를 사용가능함. (컴포넌트로 제공)
Error Boundary 컴포넌트 : ErrorBoundary라는 특수한 컴포넌트를 제공하여 에러가 발생한 경우 이를 캐치하고 대체 UI를 표시할 수 있음 => 컴포넌트 하위에서의 에러감지
Fallback UI 표시 : 에러코드나 메세지에 따라 여러 fallback UI 설정가능
에러 정보를 추적: ErrorBoundary는 에러에 대한 정보를 onError 콜백 함수를 통해 제공하여 에러를 추적하고 로깅하는 데 사용할 수 있습니다.
reset : 에러가 발생한 페이지에서 api호출 재시도기능 구현가능.
에러바운더리는 클라이언트 컴포넌트에서만 사용가능함.
여기서 에러의 종류나 상태코드에 따라 에러를 처리하도록 개발자가 설정 할 수 있다.
기본적으로 에러는 클라이언트측에서 발생한 에러와 서버에 요청할 때 상태코드에 따른 에러를 따로 처리해줘야한다.
동작방식 => componentDidCatch 생명주기 메소드에서 하위컴포넌트에서 던진 에러를 잡음 or useErrorBoundary hook을 이용해 의도적으로 에러를 발생하는 것도 가능
이후 render 메소드를 호출할 때 => didcatch가 true인 상태라면, 미리설정한 fallback을 보여주고, development환경이라면 또 다시 상위로 에러를 던짐 (production에서는 따로 에러처리를 해주지 않는다면 빈 화면만 나타남) => 개발에서 무슨 에러 났는지 알려줌
resetErrorBoundary 메소드는 결국 에러 발생이전으로 상태를 돌리는 역할을 하는 메소드임. Errorboundary의 didcatch와 error 값을 initial State로 set함
import { isDevelopment } from "#is-development";
import { Component, createElement, ErrorInfo, isValidElement } from "react";
import { ErrorBoundaryContext } from "./ErrorBoundaryContext";
import { ErrorBoundaryProps, FallbackProps } from "./types";
type ErrorBoundaryState =
| {
didCatch: true;
error: any;
}
| {
didCatch: false;
error: null;
};
const initialState: ErrorBoundaryState = {
didCatch: false,
error: null,
};
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
this.state = initialState;
}
static getDerivedStateFromError(error: Error) {
return { didCatch: true, error };
}
resetErrorBoundary(...args: any[]) {
const { error } = this.state;
if (error !== null) {
this.props.onReset?.({
args,
reason: "imperative-api",
});
this.setState(initialState);
}
}
componentDidCatch(error: Error, info: ErrorInfo) {
this.props.onError?.(error, info);
}
componentDidUpdate(
prevProps: ErrorBoundaryProps,
prevState: ErrorBoundaryState
) {
const { didCatch } = this.state;
const { resetKeys } = this.props;
// There's an edge case where if the thing that triggered the error happens to *also* be in the resetKeys array,
// we'd end up resetting the error boundary immediately.
// This would likely trigger a second error to be thrown.
// So we make sure that we don't check the resetKeys on the first call of cDU after the error is set.
if (
didCatch &&
prevState.error !== null &&
hasArrayChanged(prevProps.resetKeys, resetKeys)
) {
this.props.onReset?.({
next: resetKeys,
prev: prevProps.resetKeys,
reason: "keys",
});
this.setState(initialState);
}
}
render() {
const { children, fallbackRender, FallbackComponent, fallback } =
this.props;
const { didCatch, error } = this.state;
let childToRender = children;
if (didCatch) {
const props: FallbackProps = {
error,
resetErrorBoundary: this.resetErrorBoundary,
};
if (typeof fallbackRender === "function") {
childToRender = fallbackRender(props);
} else if (FallbackComponent) {
childToRender = createElement(FallbackComponent, props);
} else if (fallback === null || isValidElement(fallback)) {
childToRender = fallback;
} else {
if (isDevelopment) {
console.error(
"react-error-boundary requires either a fallback, fallbackRender, or FallbackComponent prop"
);
}
throw error;
}
}
return createElement(
ErrorBoundaryContext.Provider,
{
value: {
didCatch,
error,
resetErrorBoundary: this.resetErrorBoundary,
},
},
childToRender
);
}
}
function hasArrayChanged(a: any[] = [], b: any[] = []) {
return (
a.length !== b.length || a.some((item, index) => !Object.is(item, b[index]))
);
}
production 환경에서 클라이언트 UI를 그리는데 에러가 발생한다면 사용자는 흰 화면을 보고 에러발생여부를 확인 할 수 없다.
이런 경우를 대비해서, 상위 컴포넌트에서 Global ErrorBoundary로 다른 컴포넌트를 감싼 후, UI를 대체할 fallback을 보여주도록 설정해야한다.
코드예시는 대강 이런식이다.
"use client";
import { ErrorBoundary } from "react-error-boundary";
function fallbackRender({ error, resetErrorBoundary }) {
// Call resetErrorBoundary() to reset the error boundary and retry the render.
return (
<div role="alert">
<p>Something went wrong:</p>
<pre style={{ color: "red" }}>{error.message}</pre>
</div>
);
}
<ErrorBoundary
fallbackRender={fallbackRender}
onReset={(details) => {
// Reset the state of your app so the error doesn't happen again
}}
>
<ExampleApplication />
</ErrorBoundary>;
만약 서버에 api를 호출하는 과정에서 오류가 발생했다면 => 상태코드에 따라 에러메세지로 분기처리를 해주는 방법이 존재한다.
이것을 구현하는 방법은 여러가지가 있을 수 있다.
에러가 발생해 => 클라이언트측 에러가 아니라 AxiosError 라면 fallback Renderer 속성을 통해 다른 fallback을 렌더링하는 방식이다
axios에서 제공하는 isAxiosError를 사용해 에러 type을 판별 할 수 있다.
여기서 error객체의 response 상태값에 따라 어떤 에러 메세지가 발생했는지를 알 수 있다.
error 의 상태값들이 여러가지마다 모두 분기처리를 해줘야 하는 단점이 있다.
- 예를들어 api 요청을 보냈는데
400 : BAD REQUEST
401 : 로그인 필요
403 : 권한이 없음
404 : NOT FOUND
500 : INTERNAL SERVER ERROR .. 이런식의 상태 코드가 존재 할 수도 있고,
NET_WORK_ERROR 처럼 네트워크 문제일 수도 있고,
요청을 했는데 CANCELED되는 상황이 존재할 수도 있다.
<ErrorBoundary
fallbackRender={({ resetErrorBoundary, error }) => {
if (isAxiosError(error)) {
return (
<AxiosErrorFallback error={error}/>
);
}
return <ClientErrorFallback error={error}/>;
}}
onReset={handleReset}
>
{children}
</ErrorBoundary>
두번째 방식은 Axios Interceptor와 클라이언트 전역상태를 이용해 Error를 핸들링하는 방식이다.
이 방식은 좀 더 세밀하게 클라이언트 단에서 에러를 핸들링 할 수 있다.
Interceptor에서 response를 받은 후 클라이언트 store 전역상태에 에러를 저장해 놓는다.
에러를 클라이언트 전역상태에 저장 후, Promise.reject로 상위 컴포넌트로 에러를 던진다.
이러면 Global Error Boundary가 Error를 Catch 하고 실행되고
이후 다른 에러를 처리하는 모달에서 클라이언트 전역상태를 subscribe 한 뒤 에러를 가져와서 처리하는 방식이다.
전역상태관리의 store에서는 보통 subscribe 메소드가 존재한다. => atom들의 변화를 감지해 컴포넌트를 실행 할 수 있게 해준다.(jotai store의 sub함수)
조금 더 세밀한 예외처리가 가능하다. (CUSTOM 하기 편한 장점)
ref)
라이브러리 공식문서 : https://github.com/bvaughn/react-error-boundary
https://xionwcfm.tistory.com/372
https://jotai.org/docs/core/store#createstore