ErrorBoundary와 componentDidCatch

수툴리 양·2022년 12월 23일
0

TIL(2)

목록 보기
2/8

리액트에서는 에러 등의 예외가 발생하면 모든 컴포넌트를 unMount 시킨다.
이 경우 유저에게는 흰 화면만 보이게 되는데, 이러한 조치는 사용자경험에서 좋지 않다.

이를 위해 리액트는 ErrorBoundary 컴포넌트를 제공한다.

ErrorBoundary 컴포넌트

App 컴포넌트를 ErrorBoundary로 감싸 리턴한다.
하위 컴포넌트에서 발생한 예외를 감지하여 fallback UI를 보여주는 등의 역할을 한다.

<ErrorBoundary>
	<App />
</ErrorBoundary>

< ErrorBoundary 컴포넌트 구조 >

import React from "react";

import CustomFallbackUI from "./CustomFallbackUI";

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

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 에러 리포팅 서비스에 에러를 기록
    console.log(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI 렌더
      return <CustomFallbackUI />;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

*아직 react hook에서는 componentDidMatch 등의 메소드를 지원하지 않아 클래스형 컴포넌트 구조이다.


내가 하고 싶었던 작업은
ErrorBoundary 에서 fallback UI (에러 팝업 등)을 보여주기 전
새로고침을 2~3회 정도 시도 후 그 때에도 아직 에러 상태라면 에러 팝업을 보여주도록 하는 것이었다.

  • 배경
    이 때 ErrorBoundary에 react-router-dom 의 createBrowserHistory() 메소드로 생성한 값을 history라는 이름으로 넘겨주고 있었고
<ErrorBoundary history={history}>
	<App />
</ErrorBoundary>
  • 작업
    history의 location: { pathname, search } 를 활용하여 홈화면에서의 에러 등을 구분하여 처리하는 게 목적이었다.

    1. 메인 홈페이지에서 에러가 날 경우: 에러 팝업을 띄움
    2. 다른 페이지에서 에러가 날 경우: 새로고침 3회 시도

이 처리를 나는 componentDidMatch와 render 중 어디에서 처리해야 하는 것일까?

componentDidMatch()

개발자가 사전에 예외처리를 하지 않은 에러가 발생했을 때 이를 캐치하는 메소드이다.
두개의 인자를 받는데, error (state값) 과 그 외 정보값을 받을 수 있다.

render()메소드에서 state값이 true일 경우 에러팝업 등 fallback UI를 보여주어야 하는 것.

내가 하고 싶은 것은 에러가 발생한 것을 캐치하고 새로고침을 시도하는 것이므로
componentDidMatch에서 로직을 추가해야 했다.

 componentDidCatch(error, errorInfo) {
    const {
      location: { pathname, search },
    } = this.props.history

    if (에러 발생) {
      window.location.reload()
    } else if(새로고침 3) {
      this.render() // or 홈으로이동()
    }
   
    const errorData = {..}
    console.log(errorData)
  }

 render() {
    if (this.state.hasError) {
        return (
          <AlertModal show={this.state.hasError} {..} >
            오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.
          <AlertModal>
        )
    }
    return this.props.children
  }

이렇게 되면 에러 발생 시 무한반복 reload가 있게 되고 중간중간 에러팝업이 보일 것이다.

새로고침 3회 이상 여부를 어떻게 검사할 수 있을까?

  • state로 관리? X
    ErrorBoundary가 새로고침되면 새로 렌더되어 초기화되므로 카운트 업이 불가하다.
  • url에 querystring을 누적 ✓
    (좋은 방법인지는 모르겠으나) 새로고침횟수를 url에 search param으로 누적시키고, 카운팅 업하고 읽어들여 무한 새로고침을 막을 수 있도록 하였다.
 componentDidCatch(error, errorInfo) {
    const {
      location: { pathname, search },
    } = this.props.history

    const parsedSearch = qs.parse(search)      

    if (isNil(parsedSearch['reload'])) {
      const url = qs.stringifyUrl({
        url: pathname,
        query: {
          ...parsedSearch,
          reload: 1,
        },
      })

      window.location.replace(url)
    } else if (toNumber(parsedSearch['reload']) < 3) {
      parsedSearch['reload'] = toNumber(parsedSearch['reload']) + 1

      const url = qs.stringifyUrl({
        url: pathname,
        query: {
          ...parsedSearch,
        },
      })

      window.location.replace(url)
    } else {
      this.render()
    }
   
    const errorData = {..}
    console.log(errorData)
  }

 render() {
    if (this.state.hasError) {
        return (
          <AlertModal show={this.state.hasError} {..} >
            오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.
          <AlertModal>
        )
    }
    return this.props.children
  }

이 경우 window.location.replace(url) 메소드를 사용했다.
(useLocation, useHistory의 소중함을 깨달았던..)

새로고침 3회 이후 else 로 빠지게 되어 render()를 호출하게 되면 에러 팝업이 뜨는 상태가 된다.

새로고침 시에는 에러팝업을 보여주고 싶지 않다.

새로고침을 시도 하는 사이사이 ErrorBoundary 컴포넌트 렌더링 결과물이 보이게 되었다.
에러팝업을 최대한 보여주고 싶지 않은 것이 목적이었으므로
새로고침을 3회 시도하는 동안 렌더링 결과물이 null 이거나 빈 div가 되도록 수정하였다.

hasError: true

<ErrorBoundary>
	<App />
</ErrorBoundary>

App 컴포넌트를 ErrorBoundary로 감싸 호출하고 있으모로 사실 Errorboundary 컴포넌트는 에러발생과 상관없이 항상 호출되고 있는 것이다.

다만 에러가 발생했을 때 componentDidMatch()가 동작하여 hasError state를 업데이트하고, render조건에 걸려 fallback ui를 리턴하게 되는 것이다.(에러가 난 경우에만 팝업이 보이게 되는 것)

에러인 경우에 대해 뭔가 그려주기 위해서는 this.state.hasError가 true인 상태에서 뭔가 해줘야 한다.

그래서 this.state.hasError 조건과 동등한 수준에서 조건을 추가하려면, 컴포넌트 state를 추가해서만 가능할 것이고,

state가 아닌 값으로 조건을 좌우한다면 hasError 분기 하위에서 처리해야 했다.

render() {
    if (this.state.hasError) {
      const parsedSearch = qs.parse(window.location.search)
      
      if (parsedSearch['reload'] >= 3) {
         return (
          <AlertModal show={this.state.hasError} {..} >
            오류가 발생하였습니다. 잠시 후 다시 시도해 주세요.
          <AlertModal>
        )
      } else {
        return null // 새로고침하는 동안 에러팝업이 보이지 않음
      }
    }
    return this.props.children
  }

홈에서는 이 전쟁을 끝내고 싶어..

 componentDidCatch(error, errorInfo) {
    const {
      location: { pathname, search },
    } = this.props.history

    if (pathname === '/') {
      this.render()
    }
    
    const parsedSearch = qs.parse(search)      

    if (isNil(parsedSearch['reload'])) {
      const url = qs.stringifyUrl({
        url: pathname,
        query: {
          ...parsedSearch,
          reload: 1,
        },
      })

      window.location.replace(url)
    } else if (toNumber(parsedSearch['reload']) < 3) {
      parsedSearch['reload'] = toNumber(parsedSearch['reload']) + 1

      const url = qs.stringifyUrl({
        url: pathname,
        query: {
          ...parsedSearch,
        },
      })

      window.location.replace(url)
    } else {
      this.render()
    }
   
    const errorData = {..}
    console.log(errorData)
  }

render() {
    if (this.state.hasError) {
      
      const isHomePage = window.location.pathname === '/'
      const parsedSearch = qs.parse(window.location.search)
      
      if (isHomePage || parsedSearch['reload'] >= 3) {
         return (
          <AlertModal show={this.state.hasError} onConfirm={this.홈으로이동()} {..} >
            알 수 없는 오류가 발생하였습니다. 홈으로 이동하시겠습니까?
          <AlertModal>
        )
      } else {
        return null // 새로고침하는 동안 에러팝업이 보이지 않음
      }
    }
    return this.props.children
  }

특정 페이지에서 오류가 나 새로고침으로도 해결이 되지 않는다면
온전한 페이지로 유저를 데려다 놓는 게 그나마 상책일 것이다.

그래서 추가로 새로고침 이후 에러팝업에 홈으로 이동하도록 유도하는 팝업을 보여주고,
(홈으로 이동할 때에 hasError state를 업데이트해주어야 에러팝업을 다시 렌더하지 않을 것이다.)

홈화면에서 에러가 날 경우를 처리하기 위해 componentDidMatch에서 pathname을 읽어 빠르게 에러팝업을 렌더하도록 처리했다.



참고

profile
developer; not kim but Young

0개의 댓글