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;
}
}