[React] react-error-boundary 라이브러리 톺아보기 (1) - ErrorBoundary

초코침·2023년 11월 8일
3

React

목록 보기
12/14
post-thumbnail

ErrorBoundary 컴포넌트

먼저 이렇게 children 컴포넌트를 감싸 사용하는 ErrorBoundary 코드를 읽어 보면서 인상 깊었던 코드를 정리하려 합니다.

PropsWithChildren

type ErrorBoundarySharedProps = PropsWithChildren<{
  onError?: (error: Error, info: ErrorInfo) => void;
  onReset?: (
    details:
      | { reason: "imperative-api"; args: any[] }
      | { reason: "keys"; prev: any[] | undefined; next: any[] | undefined }
  ) => void;
  resetKeys?: any[];
}>;

ErrorBoundary 컴포넌트가 받는 props 중 하나입니다.

ErrorBoundary는 children을 감싸는 형태로 사용하니까 children의 타입을 props로 받을 줄 알았는데 대놓고 명시해 둔 부분이 없었습니다.

그런데 저 PropsWithChildren이 그 역할을 하는 것이었어요!

ErrorBoundarySharedProps에 커서를 올려두면, 인터섹션으로 children의 타입이 들어가 있습니다.

항상 Props 타입을 정의할 때, children: ReactNode라는 타입도 적어주곤 했는데 팁을 얻은 느낌이에요.

이 props를 넣으면 다른 props는 못쓰게 하고 싶을 때

ErrorBoundary를 써 보신 분들은 아시겠지만 fallback, FallbackComponent, fallbackRender 이렇게 비슷한 props를 제공하고 있습니다.

셋 중에 뭐를 써야 할지, 다 쓰면 좋을지 고민이 됐었는데요.

type 파일을 읽어보고 셋 중에 하나만 쓰면 된다는 걸 알게 됐습니다.

export type ErrorBoundaryPropsWithComponent = ErrorBoundarySharedProps & {
  fallback?: never;
  FallbackComponent: ComponentType<FallbackProps>;
  fallbackRender?: never;
};

export type ErrorBoundaryPropsWithRender = ErrorBoundarySharedProps & {
  fallback?: never;
  FallbackComponent?: never;
  fallbackRender: typeof FallbackRender;
};

export type ErrorBoundaryPropsWithFallback = ErrorBoundarySharedProps & {
  fallback: ReactElement<
    unknown,
    string | FunctionComponent | typeof Component
  > | null;
  FallbackComponent?: never;
  fallbackRender?: never;
};

export type ErrorBoundaryProps =
  | ErrorBoundaryPropsWithFallback
  | ErrorBoundaryPropsWithComponent
  | ErrorBoundaryPropsWithRender;

맨 위부터 3가지 타입이 fallback, FallbackComponent, fallbackRender를 모두 가지고 있는 걸 볼 수 있습니다.

다만, 셋 중 한 가지에 타입이 정확히 명시되어 있으면 다른 두 타입은 optional에 never 타입을 갖습니다.

이렇게 작성하는 이유는 여러 타입의 모든 속성을 포함하는 하위 타입을 전달하지 않게 하기 위함이라고 합니다.

(toast ui 문서에서 확인할 수 있어요.)

그러니 앞으론 셋 중 하나만 쓰면 된다고 알고 있으면 되겠습니다!

constructor 내부에서 this 바인딩

constructor 내부에서 resetErrorBoundary 함수에 this를 바인딩해 주고 있는데요.

constructor(props: ErrorBoundaryProps) {
	super(props);

	this.resetErrorBoundary = this.resetErrorBoundary.bind(this);
	this.state = initialState;
}

추측을 해 보자면, resetErrorBoundary가 일반 함수로 호출될 때 내부에 this는 전역 객체를 가리키게 될 테니

constructor 내부에서 현재 this가 가리키고 있을 미래에 생성할 인스턴스를 바인딩해서

state에 정상적으로 접근할 수 있도록 하기 위함이 아닐까 싶네요..!?

resetBoundary의 동작 방식

에러바운더리를 쓸 때 장점 중 하나가 에러가 발생했을 때 다시 시도할 수 있는 메서드를 제공한다는 점이라고 생각합니다.

resetBoundary 함수 내부를 살펴보겠습니다.

	resetErrorBoundary(...args: any[]) {
    const { error } = this.state;

    if (error !== null) {
      this.props.onReset?.({
        args,
        reason: "imperative-api",
      });

      this.setState(initialState);
    }
  }

컴포넌트의 state 중 error를 참조해서, 그 error가 null이 아니라면 (즉 에러가 발생했다면)

props로 받은 onReset 콜백 함수를 실행시키네요. 이때 onReset은 옵셔널 props기 때문에 옵셔널 체이닝 연산자가 붙었습니다.

최종적으로 컴포넌트의 state를 초기화합니다.

어떻게 다시 시도하는 건지 궁금했는데, error state를 아무일도 없었던 것처럼 초기화해서 리렌더링을 일으키는 것 같네요!

참고로 에러가 발생하면 getDerivedStateFromError 메서드를 통해 didCatcherror state가 업데이트됩니다.

static getDerivedStateFromError(error: Error) {
	return { didCatch: true, error };
}

에러 발생 시 Fallback 컴포넌트를 보여주는 동작 방식

ErrorBoundary도 결국 컴포넌트기 때문에 render 메서드 부분을 보면 어떤 컴포넌트를 렌더링할 지 확인할 수 있습니다.

조금 길지만 하나씩 읽어보겠습니다.

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 (isValidElement(fallback)) {
        childToRender = fallback;
      } else if (typeof fallbackRender === "function") {
        childToRender = fallbackRender(props);
      } else if (FallbackComponent) {
        childToRender = createElement(FallbackComponent, props);
      } 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
    );
  }

위에서 언급한 getDerivedStateFromError 메서드를 통해 에러가 발생하면 컴포넌트의 state가 didCatch: true, error: 에러 객체로 업데이트됩니다.

따라서 if (didCatch)를 통과한다면 에러가 발생한 상태일 것입니다.

if 문을 패스하고 에러가 발생하지 않은 경우를 먼저 살펴보면,

<ErrorBoundaryContext.Provider value={{didCatch, error, resetErrorBoundary}}>
	{children}
</ErrorBoundaryContext.Provider>

이렇게 children을 컨텍스트 프로바이더로 감싼 형태로 리턴하는 모습을 볼 수 있습니다.

이걸 보니 useErrorBoundary가 어떻게 생겼을지 살짝 감이 옵니다..! context의 value를 꺼내와서 활용하겠군용


이제 에러가 발생한 상태를 보겠습니다.

먼저 const props 부분은 ErrorBoundary 컴포넌트에 fallbackRender props가 채워졌을 때 사용하기 위해 만든 변수로 보입니다.

그 아래 if문을 보면 크게 fallback 관련 props를 넣은 3가지 경우와 props를 주지 않은 경우 1가지 이렇게 총 4가지를 if문으로 다루고 있음을 볼 수 있습니다.

아까 위에서 봤듯이 fallback 관련 props(fallback, fallbackRender, FallbackComponent)는 셋 중 하나만 props로 주어져야 하기 때문에 이렇게 if문으로 분기 처리를 한 것 같습니다.

  • fallback(ex. fallback={< Fallback />})이 주어진 경우: children을 fallback으로 대체
  • fallbackRender(ex. fallbackRender={(error, resetErrorBoundary)⇒ReactNode})가 주어진 경우: children을 fallbackRender 함수를 호출해 리턴받은 Element로 대체
  • FallbackComponent(ex. FallbackComponent={Fallback})가 주어진 경우: children을 FallbackComponent 함수를 호출해 리턴받은 Element로 대체

이렇게 childrenToRender을 상황에 맞게 할당한 다음, 컨텍스트 프로바이더의 children으로 렌더링합니다.

클래스형 컴포넌트가 익숙하지 않아 빠르게 휙휙 읽긴 어려웠는데, 라이브러리를 뜯어보기 전보다 확실히 시야가 넓어진 느낌은 듭니다!

다음에는 useErrorBoundary와 withErrorBoundary HOC까지 코드를 읽고 정리해보겠습니돠

profile
블로그 이사중 🚚 (https://sungjihyun.vercel.app)

0개의 댓글