-
Notifications
You must be signed in to change notification settings - Fork 1
에러 바운더리와 404 페이지 추가 #121
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
에러 바운더리와 404 페이지 추가 #121
Conversation
- 이름 수정 - 파일 분리
- group 객체에 있던 api 요청 메소드를 외부로 분리 - useMutationWithHandlers 적용 - 요청 실패시 홈으로 이동
- useMutationWithHandlers 적용
- useMutationWithHandlers 적용
- useMutationWithHandlers 적용 - api 훅에서 처리하던 onNext를 ui 코드 쪽으로 이동
- useQueryWithHandlers 적용
에러 페이지 컨텐츠를 동적으로 지정할 수 있게 하기 위함
- react-error-boundary 적용 - router의 errorElement 대신 boundary 직접 적용 - 에러에 따라 다른 에러 페이지를 보여줄 수 있도록 함 - Boundary에서 사용할 에러 타입 정의
- 해당 지출을 찾지 못한 경우 지출 목록으로 이동하도록 수정
- 에러 페이지를 사용하도록 변경 - 실제 응답의 status code인 500으로 임시 적용
- Loader 에러를 처리하기 위한 errorElement 추가 - groupTokenUrlLoader에 토큰 에러 처리 로직 추가
- 에러 페이지를 사용하도록 변경 - 주석 추가
- 기본적으로 에러를 던지도록 설정
- 에러 페이지를 사용하도록 수정 - 주석 추가
- 모임 정보 조회 API
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
무려 30커밋... 정말 고생하셨습니다!
에러 바운더리에 대해 자세히는 모르고 있었는데, 이번에 정연님 코드리뷰를 하면서 제대로 배울 수 있는 좋은 기회였습니다 👍🏻👍🏻 특히 tanstack query의 useQuery, useMutation를 커스텀해서 에러를 처리한 부분에서 라이브러리에 대한 이해도가 높다고 느꼈습니다!
이번 리뷰는 제가 사용 흐름을 잘 이해하고 있는지에 대한 질문이 다수네요😅 또, 특정 컴포넌트에서의 에러 페이지 사용에 대해 궁금증이 있어 문의 남겼습니다!
달아주신 주석들을 보니 확실히 정의되지 않거나 상황에 맞지 않는 에러 코드들이 좀 있네요. 한번 시간을 내서 같이 맞춰보면 좋을 것 같습니다! 앞으로 tanstack query를 사용하면서 손쉽게 에러 핸들링을 할 생각에 기분이 좋습니당
- errorHam, notFoundHam 이미지 너무 귀엽네요🐹 그런데 최적화를 위해서는 아마 webp로 바꿔야할 것 같습니다! 이 부분은 다른 이미지들과 함께 바꾸면 좋을 것 같네용
queryCache: new QueryCache({ | ||
onError: handleQueryError, | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제가 잘 이해했는지 궁금해서 여쭤봅니당!
1.
const { handleError: handleQueryError } = useApiError({});
const { handleError: handleMutationError } = useApiError({});
지금 handleQueryError, handleMutationError에 어떤 파라미터도 전달되지 않았으니까, defaultHandlers의 핸들러가 호출되는데, 기본으로 정해진 게 없어서 아무것도 실행되지 않는거죠?
- queries에서는 throwOnError로 상위 바운더리로 에러를 던지게만 하고, queryCache에서는 onError로 쿼리 캐시 오류를 처리하게만 하는 이유가 뭔가요? queries는 실패 시 자동 재시도를 해서 에러 처리를 하는 이유가 궁금했습니다...!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 handleQueryError, handleMutationError에 어떤 파라미터도 전달되지 않았으니까, defaultHandlers의 핸들러가 호출되는데, 기본으로 정해진 게 없어서 아무것도 실행되지 않는거죠?
맞습니다! 따로 에러를 처리하지 않고 무조건 기본 에러 페이지로 이동하게 했습니다 (throwOnError: true)
queries에서는 throwOnError로 상위 바운더리로 에러를 던지게만 하고, queryCache에서는 onError로 쿼리 캐시 오류를 처리하게만 하는 이유가 뭔가요?
queries에서 onError는 deprecated되었습니다! (onError, onSuccess, onSettled) 대신 캐시에서 처리하도록 변경되었어요.
(아래 글에 사라진 의도 같은 것들이 자세히 적혀 있는데 저도 이거 좀 더 꼼꼼히 읽어봐야 하는데 그러질 못해서... 같이 읽어보도록 합시다...ㅎㅎ)
- https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose
- https://velog.io/@cnsrn1874/breaking-react-querys-api-on-purpose
- https://xionwcfm.tistory.com/366
queries는 실패 시 자동 재시도를 해서 에러 처리를 하는 이유가 궁금했습니다...!
요거는 제가 제대로 이해한것인지 모르겠는데,,, 자동 재시도 (기본값 3) 후에도 에러가 발생하는 경우에 저희가 정의한 에러처리 로직이 돌아갑니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아하 useQuery에서 onError와 같은 사이드 이펙트를 유발하는 기능을 제거했군요..! handleQueryError, handleMutationError에서 기본 에러 페이지로 이동하는 점도 이해했습니다!
function RouteErrorElement() { | ||
const error = useRouteError(); | ||
|
||
if (error instanceof BoundaryError) { | ||
const { title, description, action } = error; | ||
return ( | ||
<ErrorPage title={title} description={description} action={action} /> | ||
); | ||
} | ||
|
||
return <ErrorPage />; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
GlobalErrorBoundary, RouteErrorBoundary, RouteErrorElement가 각각 어떤 에러를 처리하는지 잘 모르겠는데, 제가 이해한 바가 맞나요? 😵💫
- GlobalErrorBoundary: 모든 종류의 에러 -> 컴포넌트의 코드 에러, 예외 처리를 하지 않은 다른 기타 오류들
- RouteErrorBoundary: Router에서 경로 변경, 잘못된 경로 이동 등
- RouteErrorElement: checkAuth, groupTokenUrlLoader, getGroupManagerAuth 등의 loader에서 발생하는 에러들
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요거 답이 좀 늦었네요... 저도 여러 번 구조를 바꾸다 보니 조금 헷갈려서 다시 공부하고 오느라 늦어졌습니다...
GlobalErrorBoundary
는 Router 밖에서 랜더링 중 발생하는 에러를 잡습니다! 저희는 아직은 router 외부에 에러가 발생할만한 로직이 없지만... Layout
컴포넌트 같이 Router밖의 컴포넌트에서 에러가 발생할 때에는 GlobalErrorBoundary
에서 처리됩니다.
<GlobalErrorBoundary> {/* 🚨 여기서 에러 캐치! */}
<QueryClientProvider client={queryClient}>
<Layout> {/* 💥 여기서 에러 발생 */}
<GlobalStyles />
<AppRouter /> {/* 👈 여기에 Router! */}
<ReactQueryDevtools />
<Toast />
</Layout>
</QueryClientProvider>
</GlobalErrorBoundary>
RouteErrorBoundary
는 GlobalErrorBoundary
에서 잡지 못하는 Router 안에서 발생하는 에러를 잡습니다. 잘못된 경로를 처리한다기 보다는 Router를 통해서 랜더링되는 개별 경로의 페이지들을 랜더링 할 때 발생하는 에러를 처리하는거예요. 저희가 에러 발생시에 던지는 BoundaryError
들이 여기서 처리됩니당.
throw new BoundaryError({
title: '접근 권한이 없어요',
description: '모임의 총무만 참여자를 추가할 수 있어요.',
});
그런데 loader에서 발생하는 에러들은 컴포넌트 랜더링 중에 발생하는 에러가 아니기 때문에 Error Boundary로 처리가 안되더라구요. 그래서 이해하신 것처럼 RouteErrorElement
를 errorElement
에 추가해서 loader에서 발생하는 에러들을 처리할 수 있도록 했습니다...!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오오 한번에 이해했습니다!! 꼼꼼하게 작성해주셔서 감사합니당 👍🏻👍🏻
variant?: TypographyKey; | ||
color?: ColorKey; | ||
as?: ElementType; | ||
children: React.ReactNode; | ||
} | ||
|
||
function Text({ variant = 'body1R', color, as = 'span', children }: TextProps) { | ||
function Text({ | ||
className, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 커밋에 스타일 상속을 위해 className 추가라고 되어 있는데, 그럼 className으로 전달된 스타일 컴포넌트 코드에 Text의 스타일을 오버라이드하는거죠?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
제거 커밋메시지를 좀 더 잘 적었어야 했는데...
일단 요렇게 쓰기 위해서 className props를 추가해 준 거예요
(Text는 styled-component로 만든 컴포넌트가 아니기 때문에 className props가 필요해요)
import Text from '@/common/components/Text';
import styled from 'styled-components';
export const SubText = styled(Text)`
text-align: center;
white-space: pre-line;
`;
엄밀히 말하면 Text 스타일 위에 다른 스타일(SubText)로 오버라이드하기 위해서 className을 추가해 준 것입니당
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 그렇군요 확인했습니다!!
useEffect(() => { | ||
if (query.error) { | ||
handleError(query.error); | ||
} | ||
}, [query.error, handleError]); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 이렇게 useQuery 훅을 커스텀해서 error를 상태관리 할 수 있군요...! 새롭게 알아갑니당 👍🏻
// CHECK - 문서에는 401 에러로 되어있지만 실제로는 500 에러가 발생함 | ||
if (error.response?.status === 401) { | ||
throw new BoundaryError({ | ||
title: '접근 권한이 없어요', | ||
description: '참여한 모임의 정산만 확인할 수 있어요.', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이건 확실히 에러 코드가 이상하네요 한번 서버분들께 문의해봐야할 것 같습니다!
401: () => { | ||
throw new BoundaryError({ | ||
title: '접근할 수 없는 페이지예요', | ||
description: '참여한 모임의 정산만 확인할 수 있어요.', | ||
}); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 이렇게 FallbackPage에 props를 넘기는 식으로 에러 페이지를 쉽게 나타낼 수 있네요! 🫢 코드가 선언적이라 좋습니당
그런데 action이 정해져 있지 않으면 이 상황에서 사용자는 홈으로 돌아가게 되는 걸까요? 사용자가 현재 페이지에서 이동하지 않고 그대로 사용하려면 toast로도 가능하지 않을까 싶습니다!
+addAccountStep, memberBottomSheet, memberSetup 도 마찬가지입니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useGetGroupHeader
에서 홈으로 돌아가도록 한 이유는
401에러는 사용자가 잘못 입력해서 발생한 에러가 아니라 권한 문제로 발생하는 에러이기 때문입니다!
권한 문제에서는 토스트 알림을 보냈을 때 사용자가 그 화면에서 재시도해서 성공할 수 있는 방법이 있을까 싶어서
에러 페이지를 띄우고 홈 화면으로 이동하도록 했어요..
(근데 지금 생각해보니 애초에 이 페이지에 들어오면 안되는거 같기도 하네요....................)
addAccountStep
, memberBottomSheet
, memberSetup
모두 권한이 문제인 400번대 에러라서
권한에 맞는 페이지로 이동하도록 해야 할 것 같아서 홈으로 이동하도록 설정해준거예요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
권한 문제에서는 토스트 알림을 보냈을 때 사용자가 그 화면에서 재시도해서 성공할 수 있는 방법이 있을까 싶어서
에러 페이지를 띄우고 홈 화면으로 이동하도록 했어요..
아 왠지 이런 상황이 발생할 수도 있을 것 같네요......😰 그리고 해당 페이지 url로 들어오는 경우라면 에러 페이지가 확실히 필요할 것 같네요! 확인했습니당
}: ErrorPageProps) { | ||
return ( | ||
<S.Flex> | ||
<S.ErrorHamster src={errorHam} alt="error hamster" /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ErrorHam... 완전 귀여운 파일명... 🐹
|
답변 달아주신 것 모두 확인했습니다! 적용하신 에러핸들링에 대해 확실하게 이해할 수 있었어요
이건 저도 지피티에게 물어보긴 했는데, 지금도 저는 좋지만 혹시 아직 고민하신다면 이런 이름들도 괜찮을 것 같습니다! -> bypassErrorBoundary, ignoreErrorBoundary |
@ongheong 의견 감사합니다! skip보다는 ignore가 좀 더 의도적으로 바운더리를 사용하지 않는다는 느낌이 드는 것 같아서 (영어가 참 어렵네요;;;) ignoreErrorBoundary로 바꿔볼게요! |
no -> skip으로 수정한 뒤 피드백을 받고 다시 ignore로 수정함
📝 관련 이슈
MOD-8
💻 작업 내용
1. 기본 에러 페이지와 404 페이지 추가
src/pages/error/index.tsx
)src/pages/notFound/index.tsx
)2. 에러 바운더리 추가
Note
저도 아직 공부가 좀 더 필요한 부분이라... 이해가 안되시거나 좀 이상한 부분이 있다면 꼭 말씀해주세요!
에러 바운더리를 이용해서 경계 안의 에러들을 잡아 통일성 있게 처리할 수 있도록 했습니다. (react-error-boundary 라이브러리를 사용했습니다!)
에러처리를 위해서 추가한 컴포넌트들이 총 3개인데요. 요렇게 있습니다.
GlobalErrorBoundary (
src/common/components/GlobalErrorBoundary/index.tsx
)RouteErrorBoundary (
src/common/components/RouteErrorBoundary/index.tsx
)RouteErrorBoundary를 통해서 랜더링되는 컴포넌트에서 발생하는 에러는 GlobalErrorBoundary에 잡히지 않아서 Router 내에 비슷한 로직의 바운더리를 추가해줬습니다
RouteErrorElement (
src/common/components/RouteErrorElement/index.tsx
)RouteErrorBoundary에서는 loader에서 발생하는 에러를 잡지 못하더라고요. 그래서 errorElement를 이용해서 react-router의 기본 에러 바운더리에 에러가 잡혔을 때 보여줄 에러 페이지를 정의해줬습니다.
컴포넌트들이 좀 분산되어 있긴 한데 🥹 계층 구조는 대략 이렇습니다
3. API 에러 핸들러 추가
우선 지금 구현한 에러 처리 흐름을 그려봤습니다... (이해가 잘 되실지 모르겠어요 🥲🥲)
우선 API 에러 처리를 좀 일관성있게 할 수 있도록 useApiError 훅을 만들었습니다. (
src/common/hooks/useApiError.ts
)설명은 주석에 최대한... 달아두었습니다.
그리고 정의되지 않은 에러들을 처리하기 위해서 QueryClient에 핸들러를 추가했습니다. (정해진 바가 없어서 아직 비어있긴 하지만) 기본 핸들러를 useQuery와 useMutation 각각에 추가해줬고, 정의되지 않은 에러들은 기본적으로 에러 페이지를 보여주도록 throwOnError 값을 true로 설정해줬습니다.
useApiError와 useQuery, useMutation를 함께 사용하는 방법이 좀 애매한 것 같아서
상태 코드별 에러 핸들러와 ErrorBoundary를 사용하지 않을 에러를 함께 전달할 수 있는 useQueryWithHandler와 useMutationWithHandler를 만들었습니다.
사용 방법은 useQuery, useMutation과 동일한데, useApiError 훅에 전달할 인자들을 추가로 전달할 수 있도록 확장했습니당
4. 정의된 에러들에 대한 핸들러 추가
API 문서에 있는 정의된 에러들에 대한 핸들러를 추가해두었습니다!
문서와 실제 API 동작이 다른 경우가 있어서 문서를 기준으로 핸들러를 정의해뒀습니당
실제 동작까지 확인한 API들은 체크해뒀어요!
Screen_Shot.2025-04-16.12.51.58.mov
Screen_Shot.2025-04-16.12.28.04.mov
Screen_Shot.2025-04-16.12.22.34.mov
이 외에 정의되지 않은 에러들의 경우에는 백엔드분들께 한번 정리를 부탁드려야 할 것 같아요.. ㅎㅎ 주말 중에 말씀드리도록 하겠습니다!
👻 리뷰 요구사항
무엇보다 이 흐름이 너무 복잡하지 않고 이해가 되는 흐름인지가 궁금합니다...
너무 복잡하다거나, 제가 빠뜨린 부분이 있거나, 이해가 안되는 부분이 있다면 꼭 알려주세요!!!!