ErrorBoundary는 렌더링 과정 중 문제가 생겨서 화면에 빈페이지를 렌더링 할 때 이를 대체하는 UI를 렌더링 해주는 React16부터 도입된 에러처리 기술이다.
Error Boundary 공식문서
https://ko.reactjs.org/docs/error-boundaries.html
ErrorBoundary 컴포넌트를 만들고 전역 또는 필요한 부분에 컴포넌트를 사용하면 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 폴백 UI를 보여 줄 수 있다. 또한 렌더링 도중 생명주기 메서드 및 그 아래에 있는 전체 트리에서 에러를 잡아낸다.
ErrorBoundary는 기존에 사용하던 try-catch 구문 보다 효과적으로 또 쉽게 에러를 처리 할 수 있고 에러 시 렌더링 되는 컴포넌트를 쉽게 만들 수 있는 장점이 있다고 판단했다. 따라서 react-query로 api 통신을 하도록 만들었던 Todolist 프로젝트에 도입해보기로 했다.
1. ErrorBoundary 컴포넌트
- 공식문서 예시
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
//사용할 때
<ErrorBoundary>
<MyWidget />
</ErrorBoundary>
- Hooks에서 지원하는 방식으로는 오류 발생 시 제어할 방법이 없어서 ErrorBoundary는 class컴포넌트 형식으로 만들어야한다.
- getSnapshotBeforeUpdate: 가장 마지막으로 렌더링 된 결과가 DOM 등에 반영되었을 때 호출
- getDerivedStateFromError: 하위의 자손 컴포넌트에서 오류가 발생했을 때 render 단계에서 호출(렌더 단계는 렌더링 결과로 수집한 내용으로 Virtual DOM을 생성하고 이전 Virtual DOM과 비교하는 단계)
- componentDidCatch: 하위의 자손 컴포넌트에서 오류가 발생했을 때 commit 단계에서 호출(commit 단계는 React가 비교를 끝내고 DOM에 직접적으로 갱신될 내용을 적용하는 단계)
- 위 세가지 라이프사이클에서는 아직 Hook에서는 구현되지 않기 때문에 Errorboundary는 class형식으로 사용한다.
1) static getDerivedStateFromError()
static getDerivedStateFromError(error){
return { hasError: true };
}
- 하위의 자손 컴포넌트에서 오류가 발생했을 때 호출된다. 이 메서드는 매개변수로 error를 전달받고, 갱신된 state 값을 반드시 반환한다.
- 하위 컴포넌트의 렌더링 중 에러가 발생하면 static getDerivedStateFromError는 렌더 단계에서 호출된다.
- static getDerivedStateFromError 내부에는 side effects가 발생할 만한 작업을 해서는 안 된다.
- 렌더링 단계에서 side effects가 발생하면 안 되는 이유:
- 가상돔을 그리는 과정(렌더링 단계)에서는 오직 컴포넌트의 "순수한 부분"만 포함되어야 한다. 그래야 리액트의 핵심인 이전 렌더링 결과와 이번 렌더링 결과를 비교해 바뀐 것만 비교해 컴포넌트를 업데이트할 수 있기 때문이다.
- Side Effect를 야기하는 과정을 가상돔을 만드는 과정(render phrase)에 포함되면 Side Effect가 발생할 때마다 Virtual-DOM을 다시 그려야 한다. 또한 데이터 요청의 경우에는 동기적으로 네트워크 응답을 받아야만 다음 과정이 진행될 수 있다.
- hasError의 상태를 true로 변경 -> render() 메서드에서 hasError가 true일 때 fallback UI를 렌더링
2) componentDidCatch()
componentDidCatch(error, errorInfo) {
// 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
logErrorToMyService(error, errorInfo);
}
- error와 errorInfo 두 개의 매개변수를 받는다.
- error 에러에 대한 정보
- info 어떤 컴포넌트가 오류를 발생했는지에 대한 내용을 포함한 componentStack키를 갖고 있는 객체
- 에러를 기록할 때 사용 된다.
- 커밋단계에서 동작함으로 side Effect가 발생해도 된다.
- Todolist에 적용한 형태(TypeScript)
import React, { ReactNode } from "react";
export interface Props {
fallback: React.ElementType;
message?: string;
onReset?: () => void;
children?: ReactNode;
}
interface State {
hasError: boolean;
info: Error | null;
}
const initialState: State = {
hasError: false,
info: null,
};
class ErrorBoundary extends React.Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = initialState;
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, info: error };
}
onResetErrorBoundary = () => {
const { onReset } = this.props;
onReset == null ? void 0 : onReset();
this.reset();
};
reset() {
this.setState(initialState);
}
render() {
const { hasError, info } = this.state;
const { children, message } = this.props;
if (hasError) {
const props = {
error: info,
onResetErrorBoundary: this.onResetErrorBoundary,
};
return (
<this.props.fallback
onRefresh={this.reset}
onReset={props.onResetErrorBoundary}
message={message}
/>
);
}
return children;
}
}
export default ErrorBoundary;
- error와 fallback UI를 props로 받아서 어느 곳이든 사용이 가능하도록 컴포넌트화 하였다.
- error가 발생하면 props fallback UI와 error message를 받는 컴포넌트를 출력한다.
- onRefresh와 onReset은 에러를 해결하고 컴포넌트 하위에 있는 모든 쿼리 오류를 재설정한다.
- onResetErrorBoundary 함수는 onReset을 useQueryErrorResetBoundary의 reset() 함수를 onReset props로 받는다.
- 만약 onReset이 null이면 void를 return 하고 에러가 발생하고 재설정 이벤트가 발생하면 onReset함수를 실행한다.
- 함수 실행 후 this.reset()을 실행하여 initialState를 this.setState() 메서드를 사용하여 초기화한다.
- error가 발생하면 fullback UI 컴포넌트에 onReset과 onRefresh 함수를 전달하여 버튼과 같은 이벤트핸들러와 연결하고 error를 해결하고 버튼클릭시 쿼리 초기화 및 쿼리 재시작이 되도록 한다.
2. fallback UI로 사용할 Error 컴포넌트 예시
interface Props {
isRefresh?: boolean;
message?: string;
onReset?: () => void;
}
const Error = (props: Props) => {
return (
<>
<h2>페이지 오류</h2>
<p>{props.message}</p>
<button onClick={props.onReset}>새로고침</button>
</>
);
};
export default Error;
3. Errorboundary 전역에 적용
import { useRef } from "react";
import {
QueryClient,
QueryClientProvider,
useQueryErrorResetBoundary,
} from "react-query";
import ErrorBoundary from "./components/Error/ErrorBoundary";
import Router from "./router/router";
import Error from "./components/Error/Error";
function App() {
const { reset } = useQueryErrorResetBoundary();
const queryClientRef = useRef<QueryClient>();
if (!queryClientRef.current) {
queryClientRef.current = new QueryClient({
defaultOptions: {
queries: {
retry: 0,
useErrorBoundary: true,
},
},
});
}
return (
<>
<ErrorBoundary onReset={reset} fallback={Error} message="로드 실패">
<QueryClientProvider client={queryClientRef.current}>
<Router />
</QueryClientProvider>
</ErrorBoundary>
</>
);
}
export default App;
- useQueryErrorResetBoundary의 reset() 함수를 구조분해 할당으로 선언한다.
- queryClient의 defaultOptions에 useErrorBoundary 값을 true로 설정한다(Errorboundary를 사용하겠다는 뜻)
- 사용하고 싶은 곳에 ErrorBoundary 컴포넌트를 씌어준다.
<ErrorBoundary onReset={reset} fallback={Error} message="로드 실패">
//...생략
</ErrorBoundary>
- onReset은 Errorboundary컴포넌트에 reset함수를 props로 전달하고 fallback에는 에러 발생 시 렌더링 하고 싶은 UI 컴포넌트를 전달한다.