파크로그
article thumbnail
Published 2022. 8. 29. 22:29
ErrorBoundary 구현기 Frontend/⚛ React

ErrorBoundary

ErrorBoundary 를 구현했다. ErrorBoundary 를 만들어 보니 내가 원하는 곳에서 원하는 컴포넌트로 fallback 하고 싶은 상황이 무조건 생길 것이고 그 때마다 ErrorBoundary 를 만드는 것은 비효율적이라는 생각이 들었고, 이와 관련하여 한재엽님이 Suspense 와 ErrorBoundary 를 선언적으로 사용하기 와 관련된 게시글을 읽어보았다.

 

늘 그렇듯 학습을 위해 프로젝트에 바로 적용하기보다 컨셉을 하나하나 적용해가며 불편함을 겪고 넘어가려한다.. 일단은 fallback component 를 지정하여 렌더하는 방식부터 해볼 생각이다. 

 

 

설계했던 백엔드 부터

간단히 만들어서 사용하던 백엔드 응답에 문제가 있었다. 무조건 2XX 의 status code 로 응답이 오는 것.

React 의 ErrorBoundary 는 렌더링 과정의 Error 를 잡는다. 아래 과정에서는 에러를 포착하지 않는다.

 

  • 이벤트 핸들러 (더 알아보기)
  • 비동기적 코드 (예: setTimeout 혹은 requestAnimationFrame 콜백)
  • 서버 사이드 렌더링
  • 자식에서가 아닌 에러 경계 자체에서 발생하는 에러

 

useQuery 에서 onSuccess 시 ok 를 확인하고, 그 ok 가 false 이면 throw Error 를 하게 했는데 그렇게 되면 렌더링 과정의 에러로 판단하기가 어려웠다.

 

throw Error 를 한다고 해서 통신과정의 Error 로 잡지 않아 useQuery 의 useErrorBoundary option 도 먹히지 않았다.

 

  // 문제의 코드

  const { data } = useFetchMeInterval({
    onSuccess: ({ ok, message }) => {
      if (!ok) {
        throw Error(message);
      }
    },
    useErrorBoundary: true,
  });

 

// Backend 에서 사용하던 result 형식
// Error 가 나도, response 의 status code 를 별도로 지정하지 않았기에 200 으로 판별한다.

export const sendMethodResult = (
  callback: (req: Request, res: Response) => void
) => {
  const method = async (req: Request, res: Response) => {
    try {
      const results = await callback(req, res);
      res.send({
        ok: true,
        results,
      });
    } catch (error) {
      const e = error as Error;
      res.send({
        ok: false,
        message: e.message,
      });
    }
  };
  return method;
};

Server 에서 보내주는 Response 의 Status Code 를 2XX 이 아니라 에러와 관련된 Status Code 로 보내주면 useErrorBoundary 를 사용할 수있다. 해당 방식을 사용하면 onSuccess 내부에서 ok 는 무조건 true 이기 때문에 불필요한 로직이 되어 프론트에서 걷어냈다. ( 백엔드도 사실 필요없어보여서 걷어낼 예정이다. )

 

백엔드 에서는 Reponse 형태를 통일되게 하기 위해 사용하던 로직에서 status code 를 명시하여 보내줄 수 있도록 수정해야했고,

이를 다양화하여 Error Exception 관련한 것들을 만들었다.

 

// router

userRouter.get('/githubLogin', sendMethodResult(githubLogin));
userRouter.get('/me', sendMethodResult(getMe));

// sendMethodResult

export const sendMethodResult = (
  callback: (req: Request, res: Response) => void
) => {
  const method = async (req: Request, res: Response) => {
    try {
      const results = await callback(req, res);
      res.send({
        ok: true,
        results,
      });
    } catch (error) {
      const e = error as HttpException;
      res.statusCode = e.status;
      res.send({
        ok: false,
        message: e.message,
      });
    }
  };
  return method;
};

// HttpException
class HttpException extends Error {
  status: number;

  constructor(status: number, message: string) {
    super(message);
    this.status = status;
  }
}

export default HttpException;


// ForbiddenException
import HttpException from '@common/exceptions/http';

class ForbiddenException extends HttpException {
  constructor(message = '접근 권한이 없습니다.') {
    super(403, message);
  }
}

export default ForbiddenException;

 

이제 프론트에서

일단 User 의 정보를 가져오는 로직을 Header 에서 사용하기에, fallback component 를 Header 가 아닌 Outlet 에 적용하기엔 어려움이 있다고 판단하여 Header 의 UserInfo Component 를 다시 LoginButton 으로 Fallback 하도록 만들었다. 

 

지금 만든 ErrorBoundary 에서

 

1. fallback component 를 외부로부터 받아와 ErrorBoundary 를 범용적으로 사용할 수 있도록 하면 좋겠다.

2. UX 적인 측면에서 자동으로 로그아웃 되었다는 모달을 띄우면 더 좋겠다는 생각이 들었다.

 

// Header

const Header: React.FC = () => {
  const [isLogin] = useLocalStorage('isLogin', false);

  return (
    <header>
      <h1>Logo</h1>
      <div>
        {isLogin ? (
          <UserInfoErrorBoundary>
            <UserInfo />
          </UserInfoErrorBoundary>
        ) : (
          <LoginButton />
        )}
      </div>
    </header>
  );
};
  
// UserInfoErrorBoundary
  

class UserInfoErrorBoundary extends React.Component<Props, StateType> {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  render() {
    const { hasError } = this.state;
    const { children } = this.props;
    if (hasError) {
      // 재 로그인 Modal 생성하면 좋을 듯
      return <LoginButton />;
    }
    return children;
  }
}

 

profile

파크로그

@파크park

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!