Skip to content
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

[REFACTOR] 관리되지 않은 API 에러 분기 처리 개선 (UnhandledError) #408

Merged
merged 8 commits into from
Dec 3, 2024
8 changes: 6 additions & 2 deletions frontend/src/apis/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CustomError, NetworkError } from '@/utils/error';
import { CustomError, NetworkError, UnhandledError } from '@/utils/error';

interface RequestProps {
url: string;
Expand Down Expand Up @@ -26,11 +26,15 @@ const fetcher = {

return response;
} catch (error) {
if (!navigator.onLine) {
throw new NetworkError();
}

if (error instanceof CustomError) {
throw error;
}

throw new NetworkError();
throw new UnhandledError();
}
},

Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/InviteModal/InviteModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ const InviteModal = ({ isOpen, onClose, returnFocusRef }: InviteModalProps) => {
const inviteUrl = INVITE_URL(roomUuid);

const { copyToClipboard } = useClipBoard();
const { show } = useToast();
const { showToast } = useToast();

const handleCopy = () => {
copyToClipboard(inviteUrl);
show('링크가 복사되었습니다!');
showToast('링크가 복사되었습니다!');
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import DeferredComponent from '../DeferredComponent/DeferredComponent';
import AsyncErrorFallback from '../ErrorFallback/AsyncErrorFallback/AsyncErrorFallback';
import SpinnerErrorFallback from '../ErrorFallback/SpinnerErrorFallback/SpinnerErrorFallback';

import { CustomError } from '@/utils/error';
import { CustomError, UnhandledError } from '@/utils/error';

interface AsyncErrorBoundaryProps {
pendingFallback?: React.ReactNode;
Expand All @@ -25,7 +25,7 @@ const AsyncErrorBoundary = ({
<AsyncErrorFallback error={error} resetError={resetError} />
)}
onError={(error) => {
if (error instanceof CustomError) {
if (error instanceof CustomError || error instanceof UnhandledError) {
withScope((scope) => {
scope.setLevel('warning');
scope.setTag('api', 'internalServerError');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
} from '../ErrorFallback.styled';

import ErrorDdangkong from '@/assets/images/errorDdangkong.webp';
import { CustomError } from '@/utils/error';

interface AsyncErrorFallback {
error: unknown;
Expand All @@ -22,7 +21,7 @@ const AsyncErrorFallback = ({ error, resetError }: AsyncErrorFallback) => {
return (
<section css={errorFallbackLayout}>
<img src={ErrorDdangkong} alt="에러나서 슬픈 땅콩" css={errorImage} />
<h2 css={errorText}>{error instanceof CustomError && error.message}</h2>
<h2 css={errorText}>{error instanceof Error && error.message}</h2>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 질문 💭

그럼 여기서 원래는 CustumError만 잡았는데 이제는 Error로 더 넓게 잡는건가요 .. ?

Copy link
Contributor Author

@rbgksqkr rbgksqkr Dec 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

음 사실 정확히는 error instanceof CustomError || error instanceof UnhandledError 로 잡는 게 맞는데 message만 사용하기도 하고, 한줄로 표현하고 싶어서 Error로 잡았던 것 같아요!

근데 썬데이말듣고 생각해보니 나중에 헷갈릴 수도 있을 것 같네용
error instanceof CustomError || error instanceof UnhandledError 로 명확하게 표현하겠습니다!

<div css={fallbackButtonContainer}>
<Button onClick={resetError} text="다시 시도" size="medium" radius="medium" />
<Button onClick={goToHome} text="홈으로" size="medium" radius="medium" />
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/layout/Header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const TitleHeader = ({ title }: HeaderProps) => (

// 3. 가운데 제목, 우측 상단 차지하는 헤더 : 게임 대기 화면
export const RoomSettingHeader = ({ title }: HeaderProps) => {
const { show } = useModal();
const { showModal } = useModal();
const {
member: { isMaster },
} = useGetUserInfo();
Expand All @@ -75,11 +75,11 @@ export const RoomSettingHeader = ({ title }: HeaderProps) => {
const focusRef = useFocus<HTMLElement>();

const handleClickRoomSetting = () => {
show(RoomSettingModal, { returnFocusRef });
showModal(RoomSettingModal, { returnFocusRef });
};

const handleClickExit = () => {
show(AlertModal, { message: '정말로 나가시겠습니까?', onConfirm: handleExit });
showModal(AlertModal, { message: '정말로 나가시겠습니까?', onConfirm: handleExit });
};

return (
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/constants/errorStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const SERVER_ERROR_STATUS = 500;
export const NETWORK_ERROR_STATUS = 5001;
export const UNHANDLED_ERROR_STATUS = 5002;
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ import useModal from '@/hooks/useModal';

const ReadyMembersContainer = () => {
const { members, master } = useGetRoomInfo();
const { show } = useModal();
const { showModal } = useModal();
const queryClient = useQueryClient();
const returnFocusRef = useRef<HTMLButtonElement>(null);
const memberCountMessage = `총 인원 ${members.length}명`;

const handleClickInvite = () => {
show(InviteModal, { returnFocusRef });
showModal(InviteModal, { returnFocusRef });
};

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ const RoomSetting = () => {
const {
member: { isMaster },
} = useGetUserInfo();
const { show } = useModal();
const { showModal } = useModal();

const screenReaderRoomSetting = `
방 정보.
Expand All @@ -30,7 +30,7 @@ const RoomSetting = () => {
제한시간 ${roomSetting.timeLimit / 1000}초.`;

const handleClickCategory = () => {
show(RoomSettingModal, { returnFocusRef });
showModal(RoomSettingModal, { returnFocusRef });
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ const NextRoundButton = () => {
const {
member: { isMaster },
} = useGetUserInfo();
const { show } = useModal();
const { showModal } = useModal();

const returnFocusRef = useRef<HTMLButtonElement>(null);

const randomRoundNextMessage = createRandomNextRoundMessage();
const isLastRound = balanceContent?.currentRound === balanceContent?.totalRound;

const showModal = () => {
show(AlertModal, { message: randomRoundNextMessage, onConfirm: moveNextRound, returnFocusRef });
const handleNextRoundModal = () => {
showModal(AlertModal, {
message: randomRoundNextMessage,
onConfirm: moveNextRound,
returnFocusRef,
});
};

return (
Expand All @@ -35,7 +39,7 @@ const NextRoundButton = () => {
ref={returnFocusRef}
style={{ width: '100%' }}
text={getNextRoundButtonText(isMaster, isLastRound, isPending || isSuccess)}
onClick={isLastRound ? moveNextRound : showModal}
onClick={isLastRound ? moveNextRound : handleNextRoundModal}
disabled={!isMaster || isPending || isSuccess}
/>
</div>
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/providers/ModalProvider/ModalProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ interface Modal extends ModalProps {
}

interface ModalDispatchContextProps {
show: (Component: React.FC<ModalState> | null, props?: ModalProps) => void;
showModal: (Component: React.FC<ModalState> | null, props?: ModalProps) => void;
close: () => void;
}

Expand All @@ -33,7 +33,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => {
onConfirm: () => {},
});

const show = (Component: React.FC<ModalState> | null, props?: ModalProps) => {
const showModal = (Component: React.FC<ModalState> | null, props?: ModalProps) => {
setModal({
Component,
title: props?.title,
Expand All @@ -52,7 +52,7 @@ const ModalProvider = ({ children }: PropsWithChildren) => {
}));
};

const dispatch = useMemo(() => ({ show, close }), []);
const dispatch = useMemo(() => ({ showModal, close }), []);

return (
<ModalDispatchContext.Provider value={dispatch}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import { useQueryClient } from '@tanstack/react-query';
import { PropsWithChildren } from 'react';

import AlertModal from '@/components/AlertModal/AlertModal';
import { NETWORK_ERROR_STATUS, SERVER_ERROR_STATUS } from '@/constants/errorStatus';
import useModal from '@/hooks/useModal';
import useToast from '@/hooks/useToast';
import { CustomError, NetworkError } from '@/utils/error';
import { CustomError, NetworkError, UnhandledError } from '@/utils/error';

const isServerError = (status: number) => status >= 500 && status !== 555;
const isServerError = (status: number) =>
status >= SERVER_ERROR_STATUS && status !== NETWORK_ERROR_STATUS;

// QueryClient는 모든 Provider에 공유되면서 공통 에러 핸들링 로직에 Toast와 Modal을 넣기 위해 setDefaultOptions 사용
// 테스트 환경에서 retry 값이 있을 경우 에러 폴백 테스트가 돌지 않아 분기 처리
const QueryClientDefaultOptionProvider = ({ children }: PropsWithChildren) => {
const queryClient = useQueryClient();
const { show } = useToast();
const { show: showModal } = useModal();
const { showToast } = useToast();
const { showModal } = useModal();

queryClient.setDefaultOptions({
queries: {
Expand All @@ -23,9 +25,14 @@ const QueryClientDefaultOptionProvider = ({ children }: PropsWithChildren) => {
mutations: {
onError: (error) => {
if (error instanceof NetworkError) {
show(error.message);
showToast(error.message);
return;
}

if (error instanceof UnhandledError) {
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💭 질문 💭

해당 분기가 없으면 unhandledError도 모달이 뜨기 때문에 처리를 해 준 건가요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 맞습니당

}

showModal(AlertModal, { title: '에러', message: error.message });
},
throwOnError: (err) => {
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/providers/ToastProvider/ToastProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { createPortal } from 'react-dom';
import { toastLayout } from './ToastProvider.styled';

interface ToastContext {
show: (message: string) => void;
showToast: (message: string) => void;
}

export const ToastContext = createContext<ToastContext | null>(null);
Expand All @@ -13,7 +13,7 @@ const ToastProvider = ({ children }: PropsWithChildren) => {
const [toastMessage, setToastMessage] = useState('');
const timerRef = useRef<NodeJS.Timeout | null>(null);

const show = useCallback((message: string) => {
const showToast = useCallback((message: string) => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
Expand All @@ -34,7 +34,7 @@ const ToastProvider = ({ children }: PropsWithChildren) => {
}, []);

return (
<ToastContext.Provider value={{ show }}>
<ToastContext.Provider value={{ showToast }}>
{children}
{toastMessage &&
createPortal(
Expand Down
8 changes: 7 additions & 1 deletion frontend/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { NETWORK_ERROR_STATUS, UNHANDLED_ERROR_STATUS } from '@/constants/errorStatus';
import { ERROR_MESSAGE } from '@/constants/message';
import { ErrorCode } from '@/types/error';

Expand All @@ -20,6 +21,11 @@ export class CustomError extends Error {
}

export class NetworkError extends Error {
status = 555;
status = NETWORK_ERROR_STATUS;
message = '네트워크가 불안정해요. 다시 시도해주세요!';
}

export class UnhandledError extends Error {
status = UNHANDLED_ERROR_STATUS;
message = '예기치 못한 에러가 발생했어요. 관리자에게 문의 바랍니다.';
}