Error Boundary

HSKwon·2023년 5월 19일
0
post-thumbnail

React Error Boundary

1. Intro

✅ 정상적인 화면

❌ 에러 발생

2. Error Boundary

  • React는 16버전부터 컴포넌트에서 발생하는 에러를 기록하며, 깨진 컴포넌트 대신 fallback UI를 보여주는 ErrorBoundary라는 개념을 도입하였습니다.
  • ErrorBoundary를 통해 컴포넌트에서 에러가 발생했을 때, 이를 캐치하고, 사용자들에게 에러가 발생하여 앱이 중단되는 것이 아닌 다른 대체 화면을 보여줄 수 있습니다.
  • ErrorBoundaryfallback UI를 보여줄 필요성이 있는 컴포넌트를 감싸주고, 에러 발생 시 사용자에게 대체 화면을 보여준다면 에러를 적절히 핸들링하여 사용자의 경험이 향상될 수 있을 될 것이라고 생각합니다.

같은 API를 통해 응답을 받아오더라도 상황에 맞는 처리가 필요하다고 생각합니다.

  • A 라는 콘텐츠에 100% 의존하는 페이지와 A 라는 콘텐츠에 10% 의존하는 페이지가 있다고 가정했을때, A 라는 콘텐츠에 100% 의존하는 화면에 에러가 발생하면, 에러 페이지로 리다이렉트 되는것이 사용성에 더 좋다고 생각합니다.

  • 반면, A 라는 콘텐츠에 일부만 의존하는 화면에서 에러가 발생하면 ErrorBoundary를 이용해 에러가 발생했음을 화면에 띄워주는것이 좋다고 생각합니다. 유저가 A라는 콘텐츠 이외에 다른 콘텐츠를 이용하고 있을 수 있기 때문입니다.

3. 프로젝트에서의 ErrorBoundary

  • 상위 queryClient - useErrorBoundary 옵션 설정
export const queryClient = new QueryClient({ defaultOptions: queryConfig });

export const useQuery = <
  TQueryFnData = unknown,
  TError = unknown,
  TData = TQueryFnData,
  TQueryKey extends QueryKey = QueryKey,
>(
  options: UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
): UseQueryResult<TData, TError> => {
  return useQueryOrigin({
    ...options,
// ✅ 에러 발생 시 errorBoundary를 사용하여 에러처리할지 결정
// 기본적으로 useErrorBoundary는 false
    **useErrorBoundary: !options.onError,** 
    suspense: !options.onError,
  });
};
import client from '@/lib/client';
**import { useQuery } from '@/lib/react-query';**
import { ArticleDto } from '../types';

const getArticle = async (id: string): Promise<ArticleDto> => {
  return client.get(`/api/articles/${id}`);
};

export const useArticle = ({ id }: { id: string }) => {
  const { data: article, ...rest } = **useQuery**({
    queryKey: ['article', id],
    queryFn: () => getArticle(id),
  });

  if (!article) {
    throw () => getArticle(id);
  }

  return { article, ...rest };
};

3-1. useErrorBoundary 설정에 대한 정리

  • useErrorBoundary : !options.onError ⇒ (options.onError가 없을 경우에만 useErrorBoundary가 true)
    • 자식 컴포넌트의 API 쿼리의 옵션에 별도의 useErrorBoundary존재하지 않으면
      • 이 경우 자식 API 호출 쿼리에서 useErrorBoundary를 따로 설정하지 않아도 기본적으로 true로 동작합니다.
    • 자식 컴포넌트의 API 쿼리의 옵션에 별도로 useErrorBoundary존재하면
      • 자식 API 호출 쿼리에서 useErrorBoundary를 따로 설정하지 않는 한 false로 유지됩니다.

  • 결국 상위 useQuery 훅에서 useErrorBoundary : !options.onError를 설정하면, 자식 API 호출 쿼리에서 별도의 useErrorBoundary 설정이 없는 경우, 상위 옵션 상속을 받습니다.
import React from 'react';

interface ErrorBoundaryProps {
  children: React.ReactNode;
  fallback:
    | React.ReactNode
    | ((message: string, refresh: () => void) => React.ReactNode);
}

interface ErrorBoundaryState {
  hasErr: boolean;
  message: string;
}

export class ErrorBoundary extends React.Component<
  ErrorBoundaryProps,
  ErrorBoundaryState
> {
// ✅ constructor를 통해 초기 상태를 설정 
// (초기값은 에러발생❌, 메세지는 빈 문자열)
  constructor(props: ErrorBoundaryProps) {
    super(props);
    **this.state = { hasErr: false, message: '' };
  }

// ✅ getDerivedStateFromError 메서드를 통해 에러가 발생하면 
// hasErr과 message state를 업데이트
  static getDerivedStateFromError(err: Error) {
    return { hasErr: true, message: err.message ?? err.toString() };
  }

  handleRefresh = () => {
    window.location.reload();
  };

// ✅ render 부분에서는 업데이트된 hasErr 상태를 통해
// 정상 컴포넌트를 렌더링할지, fallback을 렌더링할지 결정한다
  render() {
// ✅ 에러가 발생했을 때 대신 렌더링할 컴포넌트
    const { fallback } = this.props;

// ✅ 에러가 발생하지 않았을 때 렌더링할 자식 컴포넌트
    if (!this.state.hasErr) {
      return this.props.children;
    }

    if (typeof fallback === 'function') {
      return fallback(this.state.message, this.handleRefresh);
    }

    return fallback;
  }
}

이러한 ErrorBoundary 컴포넌트 를 통해 자식 컴포넌트에서 발생하는 에러를 캐치하여 UI를 안정적으로 유지할 수 있습니다.

4. 사용하기

상위 useQuery에서 useErrorBoundary: !options.onError로 옵션을 설정한 상태

  • 자식 API 호출 쿼리에서 useErrorBoundary 옵션을 제거하거나 true로 설정
  • 이를 이용하여 API 응답을 받아 화면을 구성하고 있는 컴포넌트를 만들어둔 error boundary 컴포넌트로 감싸주면 됩니다.
// ✅ 메인 페이지의 "새로운 피드" 컴포넌트에 대한 에러처리
<ContentsBoxLayout>
    <LandingTitle
          logo={<Image path="/asset/icon/new 1.png" />}
          title="새로운 피드"
          extra={<MoreButton path="/article/feed" />}
     />
        <ErrorBoundary fallback={<ErrorFallback />}>
           <FeedList />
        </ErrorBoundary>
 </ContentsBoxLayout>

이 코드들에서는 에러상태를 관리하여, 에러가 발생할 경우 에러메세지를 보여주는 컴포넌트를 렌더링해서 화면에 에러를 표시합니다.

5. 서버 점검 화면 보여주기

마찬가지로 ErrorBoundary를 이용해 루트레벨에서 서버 점검 과 같이 페이지 전체에 에러를 보여줘야 할 경우, GlobalErrorBoundary 컴포넌트를 만들어서 점검 화면을 보여줄 수 있습니다.

class GlobalErrorBoundary {
	render() {
		if (!this.state.hasErr) {
		   return this.props.children;
		}

		if(서버 점검 에러 상황에 관한 코드) {
			return (
				// ✅ 서버 점검을 알리는 컴포넌트
				<Maintenance />
			)
		}
		return (
			...
			...
		)
	}
}

6. 다시 시도하기

단순히 에러를 캐치해서 에러 컴포넌트를 보여주는 것 이외에도, 다시 시도 버튼을 만들어 에러 발생 시 사용자에게 직접 다시 API 호출을 시도할 권한을 부여하여 에러를 리셋할 수 있도록 처리할 수 있습니다.

  • 카카오페이지 errorBoundary 적용 예시
profile
공부한 내용이나 관심 있는 정보를 글로 정리하며 익숙하게 만들고자 합니다.

0개의 댓글