카테고리 없음

[React] Error Boundary를 사용한 에러처리 (react-query)

싯타마 2023. 2. 12. 10:04

ErrorBoundary는 렌더링 과정 중 문제가 생겨서 화면에 빈페이지를 렌더링 할 때 이를 대체하는 UI를 렌더링 해주는 React16부터 도입된 에러처리 기술이다.

 

Error Boundary 공식문서

https://ko.reactjs.org/docs/error-boundaries.html

 

에러 경계(Error Boundaries) – React

A JavaScript library for building user interfaces

ko.reactjs.org

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 컴포넌트를 전달한다.