Web/React

error boundary로 행복하게 에러처리하기 (react-query)

verbena 2024. 11. 5.

UI의 한 부분에서의 자바스크립트 에러가 전체 앱을 망가뜨려서는 안된다. 

에러 처리를 어떻게 공통화해야 할 지 골머리를 앓았다.

하염없이 try catch만을 붙이는건 정말 비생산적인 일임을 깨닫고 error boundary를 본격적으로 도입했다. 

익숙하게 사용하던 axios와 react query를 계속 사용하는 것을 전제로 error 처리를 했다. 

1. axios interceptor에서는 별다른 에러 처리를 하지 않는다. 

2. 전역으로 사용할 Errorboundary를 설정한다. 

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true, error };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }
  
  // reset logic
  resetErrorBoundary = () => {
    this.props.onReset();
    
    this.setState({
      hasError: false,
      error: null,
    });
  };
  
  render() {
    const { state, props } = this;

    const { hasError, error } = state;

    const { fallback, children } = props;

    const fallbackProps = {
      error,
      reset: this.resetErrorBoundary,
    };
    
    const fallbackComponent = createElement(fallback, fallbackProps);

    return hasError ? fallbackComponent : children;

  }
}

3. 전역 error boundary로 app 컴포넌트를 감싸준다. 

4. app component 아래에 있는 구성 요소의 오류를 error boundary가 포착한다.

 

이때 errorboundary 하위 트리에서 react-query 라이브러리를 통해 서버의 상태를 호출하고자하면 useSuspenseQuery를 사용하자

useSuspenseQuery
const result = useSuspenseQuery(options)
Options
The same as for useQuery except for:
throwOnError, enabled, placeholderData

useSuspensQuery를 사용하면 error를 별다른 option 없이 바로 errorboundary에 전파해주고 suspense도 알아서 적용된다. 

또한 서버 통신이 이루어진 곳에 api 에러 처리만을 위한 errorboundary만 설정하고 싶다면 별개로 만드는 것도 가능하다.

class ApiErrorBoundary extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      shouldHandleError: false,
      shouldRethrow: false,
      error: null,
    };
  }

  static getDerivedStateFromError(error) {
    return { shouldHandleError: true, shouldRethrow: false, error };
  }

  componentDidCatch(error, errorInfo) {
    console.log("API error boundary:", { error, errorInfo });
  }

  resetErrorBoundary = () => {
    this.setState({ shouldHandleError: false, error: null });

    if (this.props.onReset) {
      this.props.onReset();
    }
  };

  render() {
    const { shouldHandleError, shouldRethrow, error } = this.state;
    const { children } = this.props;

    if (shouldRethrow) {
      throw error;
    }
    
    if (!shouldHandleError) {
      return children;
    }
    
    return (
      <this.props.fallback onReset={this.resetErrorBoundary} error={error} />
    );
  }
}

export default ApiErrorBoundary;

그다음 이 ApiErrorBoundary로 적용해주고자 하는 컴포넌트를 감싸주면 된다. 

<QueryErrorResetBoundary>
      {({ reset }) => {
        return (
            <ApiErrorBoundary  fallback={apiErrorUI} onReset={reset}>
                <Suspense fallback={fallback ?? <Loading />}><Component /></Suspense>
            </ApiErrorBoundary>
        );
      }}
</QueryErrorResetBoundary>

 

이때 component 안에 suspense까지 넣어준다면 useSuspensQuery를 호출할때 로딩 fallback도 함께 적용해줄 수 있다. 

마지막으로 새로고침과 같은 reset 로직을 적용할때 react-query의 QUeryErrorResetBoundary를 사용하여 오류가 났던 쿼리를 재호출 할 수 있다.

reset 버튼을 누르는 로직에서 한가지 주의할 점이 있는데 apiErrorBoundary에서 global로 넘긴 api호출은 global error UI에서 아무리 reset을 누른다 하더라도 api error boundary에서 fallback된 ui는 여전히 에러 상태로 남아있다. 

따라서 global error boundary에서 reset버튼을 누를때 그냥 깔끔하게 queryClient.clear()를 함께 적용해주자. 

 

프로젝트를 할때마다 try catch로 에러를 잡아주는게 영 아니다 싶어서 열심히 조사해가면서 나름 깔끔하게 에러처리를 해봤다. 

 

번외) next에서는 따로 error와 loading ui를 좀 더 편리하게 쓸 수 있는데 논리 자체는 똑같아 보인다. 

댓글