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

[FE] react-query에 맞는 error handling 적용 #415

Merged
merged 38 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
d351175
chore: 디자인시스템 버전 적용
Todari Aug 19, 2024
ca35386
rename: 파일 이름 변경
Todari Aug 19, 2024
eab610b
fix: 사용하지 않는 Error 처리 로직 삭제
Todari Aug 19, 2024
a439e5f
feat: 전역 에러 상태 생성
Todari Aug 19, 2024
224941e
fix: 바뀐 파일 이름 적용
Todari Aug 19, 2024
910b555
fix: ToastProvider 내에서 에러 처리 로직 제거
Todari Aug 19, 2024
59597a7
refactor: AppErrorBoundary 및 QueryClientBoundary를 통한 에러 처리
Todari Aug 19, 2024
296205c
fix: 사용하지 않는 코드 임시적으로 주석 처리
Todari Aug 19, 2024
7e964e1
mover: AppErrorBoundary, QueryClientBoundary 코드 위치 변경
Todari Aug 20, 2024
78bb088
fix: test 코드 변경
Todari Aug 20, 2024
3fa2cfa
chore: jest path alias 적용
Todari Aug 20, 2024
f32b9b6
test: AppErrorBoundary 테스트코드 작성
Todari Aug 20, 2024
031d8b5
remove: 사용하지 않는 코드 삭제
Todari Aug 20, 2024
6a9aabc
fix: 사용하지 않는 코드 주석처리
Todari Aug 20, 2024
84aa761
fix: AppErrorBoundary, QueryClientBoundary 로직 수정
Todari Aug 20, 2024
bc5d57d
test: AppErrorBoundary test코드 작성
Todari Aug 20, 2024
90229d4
style: lint 적용
Todari Aug 20, 2024
aec4174
feat: App에 ErrorBoundary 추가
pakxe Aug 22, 2024
785b82f
feat: UnhandledErrorBoundary 컴포넌트 구현
pakxe Aug 22, 2024
be305a1
feat: 에러를 구독하며 핸들링되는 에러면 토스트, 핸들링 불가능한 에러면 에러 바운더리를 띄우도록 하는 캐처 구현
pakxe Aug 22, 2024
facd363
feat: 에러 페이지에 메일 추가
pakxe Aug 22, 2024
eb81923
feat: errorInfo를 안에서 구현하는 것이 아닌 구현된 것을 인자로 받도록 수정
pakxe Aug 22, 2024
39cc30b
chore: 파일 위치 수정에 따라 import 경로 수정
pakxe Aug 22, 2024
727bb81
chore: 불필요해진 파일 제거
pakxe Aug 22, 2024
0c415e6
Merge branch 'fe-dev' of https://github.com/woowacourse-teams/2024-ha…
pakxe Aug 22, 2024
9262326
chore: 개행 추가
pakxe Aug 22, 2024
fff00ad
feat: ErrorCatcher에 대한 테스트 코드 작성
pakxe Aug 22, 2024
ed5e847
feat: Toast의 showingTime 을 옵셔널로 수정
pakxe Aug 22, 2024
fd723ff
chore: 없어진 컴포넌트를 renderHook에서 제거
pakxe Aug 22, 2024
4c2fee2
refactor: return문이 반복되는 부분을 리펙터링
pakxe Aug 22, 2024
973e4eb
feat: useToast에 대한 테스트코드 작성
pakxe Aug 22, 2024
2fe3e97
chore: 사용하지 않는 파일 제거
pakxe Aug 22, 2024
f6f24d2
chore: 불필요한 파일이 커버리지에 뜨지 않도록 ignore에 추가
pakxe Aug 22, 2024
7aa7ae8
Merge branch 'fe-dev' of https://github.com/woowacourse-teams/2024-ha…
pakxe Aug 22, 2024
a8b8297
rename: 파일명 오타 수정
pakxe Aug 22, 2024
ce53134
chore: import 경로 수정
pakxe Aug 22, 2024
3b55284
chore: 병힙
pakxe Aug 22, 2024
056aee1
chore: 라이브러리 오류로 인해 테스트 통과가 안되므로 디자인 시스템 라이브러리 다운그레이드
pakxe Aug 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions client/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ const config: Config = {
'<rootDir>/src/errors/',
'<rootDir>/src/components/',
'<rootDir>/src/store/',
'<rootDir>/src/pages/',
'<rootDir>/src/hooks/queries',
],

verbose: true,
Expand All @@ -29,6 +31,7 @@ const config: Config = {
'@/(.*)$': '<rootDir>/src/$1', // path alias를 적용하기 위함
'^@apis/(.*)$': '<rootDir>/src/apis/$1',
'^@constants/(.*)$': '<rootDir>/src/constants/$1',
'^@components/(.*)$': '<rootDir>/src/components/$1',
'^@hooks/(.*)$': '<rootDir>/src/hooks/$1',
'^@utils/(.*)$': '<rootDir>/src/utils/$1',
'^@pages/(.*)$': '<rootDir>/src/pages/$1',
Expand Down
27 changes: 12 additions & 15 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import {Outlet} from 'react-router-dom';
import {HDesignProvider} from 'haengdong-design';
import {Global} from '@emotion/react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';

import {ToastProvider} from '@hooks/useToast/ToastProvider';
import {ErrorProvider} from '@hooks/useError/ErrorProvider';
import QueryClientBoundary from '@components/QueryClientBoundary/QueryClientBoundary';
import ErrorCatcher from '@components/AppErrorBoundary/ErrorCatcher';

import UnhandledErrorBoundary from './UnhandledErrorBoudnary';
import {GlobalStyle} from './GlobalStyle';
import UnhandledErrorBoundary from './UnhandledErrorBoundary';

const queryClient = new QueryClient();

const App: React.FC = () => {
return (
<HDesignProvider>
<QueryClientProvider client={queryClient}>
<UnhandledErrorBoundary>
<Global styles={GlobalStyle} />
<ErrorProvider>
{/* <ErrorProvider callback={toast}> */}
<ToastProvider>
<UnhandledErrorBoundary>
<Global styles={GlobalStyle} />
<ToastProvider>
<ErrorCatcher>
<QueryClientBoundary>
<Outlet />
</ToastProvider>
</ErrorProvider>
</UnhandledErrorBoundary>
</QueryClientProvider>
</QueryClientBoundary>
</ErrorCatcher>
Copy link
Contributor

Choose a reason for hiding this comment

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

컴포넌트 명 너무 좋다! 에러를 캐치해주는 녀석

Copy link
Contributor

Choose a reason for hiding this comment

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

캣쮜

</ToastProvider>
</UnhandledErrorBoundary>
</HDesignProvider>
);
};
Expand Down
2 changes: 1 addition & 1 deletion client/src/apis/fetcher.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ErrorInfo} from '@hooks/useError/ErrorProvider';
import {ErrorInfo} from '@components/AppErrorBoundary/ErrorCatcher';

import objectToQueryString from '@utils/objectToQueryString';

Expand Down
92 changes: 92 additions & 0 deletions client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {render, screen, waitFor} from '@testing-library/react';
import {act, ReactNode} from 'react';
import {MemoryRouter} from 'react-router-dom';
import {HDesignProvider} from 'haengdong-design';

import FetchError from '@errors/FetchError';
import {ToastProvider} from '@hooks/useToast/ToastProvider';

import {useAppErrorStore} from '@store/appErrorStore';

import {SERVER_ERROR_MESSAGES} from '@constants/errorMessage';

import UnhandledErrorBoundary from '../../UnhandledErrorBoudnary';

import ErrorCatcher from './ErrorCatcher';

// 테스트용 헬퍼 컴포넌트
const TestComponent = ({triggerError}: {triggerError: () => void}) => {
return <button onClick={triggerError}>Trigger Error</button>;
};

const setup = (ui: ReactNode) =>
render(
<HDesignProvider>
<UnhandledErrorBoundary>
<ToastProvider>
<ErrorCatcher>
<MemoryRouter>{ui}</MemoryRouter>
</ErrorCatcher>
</ToastProvider>
</UnhandledErrorBoundary>
</HDesignProvider>,
);

describe('ErrorCatcher', () => {
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: jest.fn(),
}));

it('핸들링 가능한 에러인 경우 토스트가 표시된다.', async () => {
const errorCode = 'EVENT_NOT_FOUND';
const error = new FetchError({
errorInfo: {errorCode, message: '서버의 에러메세지'},
name: errorCode,
message: '에러메세지',
status: 200,
endpoint: '',
method: 'GET',
requestBody: '',
});

const {updateAppError} = useAppErrorStore.getState();

setup(<TestComponent triggerError={() => updateAppError(error)} />);

act(() => {
screen.getByText('Trigger Error').click();
});

const errorMessage = SERVER_ERROR_MESSAGES[errorCode];

await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument();
});
});

it('핸들링 불가능한 에러인 경우 에러 바운더리가 표시된다.', async () => {
const errorCode = '모르겠는 에러';
const error = new FetchError({
errorInfo: {errorCode, message: '모르겠는 에러메세지'},
name: errorCode,
message: '에러메세지',
status: 400,
endpoint: '',
method: 'GET',
requestBody: '',
});

const {updateAppError} = useAppErrorStore.getState();

setup(<TestComponent triggerError={() => updateAppError(error)} />);

act(() => {
screen.getByText('Trigger Error').click();
});

await waitFor(() => {
expect(screen.getByText('알 수 없는 오류입니다.')).toBeInTheDocument();
});
});
});
62 changes: 62 additions & 0 deletions client/src/components/AppErrorBoundary/ErrorCatcher.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {useEffect} from 'react';

import FetchError from '@errors/FetchError';
import {useToast} from '@hooks/useToast/useToast';

import {useAppErrorStore} from '@store/appErrorStore';

import {captureError} from '@utils/captureError';

import {SERVER_ERROR_MESSAGES, UNKNOWN_ERROR} from '@constants/errorMessage';

export type ErrorInfo = {
errorCode: string;
message: string;
};

const convertAppErrorToErrorInfo = (appError: Error) => {
if (appError instanceof Error) {
const errorInfo =
appError instanceof FetchError ? appError.errorInfo : {errorCode: appError.name, message: appError.message};

return errorInfo;
} else {
const errorInfo = {errorCode: UNKNOWN_ERROR, message: JSON.stringify(appError)};

return errorInfo;
}
};

const isUnhandledError = (errorInfo: ErrorInfo) => {
if (errorInfo.errorCode === 'INTERNAL_SERVER_ERROR') return true;

return SERVER_ERROR_MESSAGES[errorInfo.errorCode] === undefined;
};

const ErrorCatcher = ({children}: React.PropsWithChildren) => {
const {appError} = useAppErrorStore();
const {showToast} = useToast();

useEffect(() => {
if (appError) {
const errorInfo = convertAppErrorToErrorInfo(appError);
captureError(appError, errorInfo);

if (!isUnhandledError(errorInfo)) {
showToast({
showingTime: 3000,
message: SERVER_ERROR_MESSAGES[errorInfo.errorCode],
type: 'error',
position: 'bottom',
bottom: '8rem',
});
} else {
throw appError;
}
}
}, [appError]);

return children;
};

export default ErrorCatcher;
24 changes: 24 additions & 0 deletions client/src/components/QueryClientBoundary/QueryClientBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {MutationCache, QueryCache, QueryClient, QueryClientProvider} from '@tanstack/react-query';

import {useAppErrorStore} from '@store/appErrorStore';

const QueryClientBoundary = ({children}: React.PropsWithChildren) => {
const {updateAppError} = useAppErrorStore();

const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: (error: Error) => {
updateAppError(error);
},
}),
mutationCache: new MutationCache({
onError: (error: Error) => {
updateAppError(error);
},
}),
Comment on lines +9 to +18
Copy link
Contributor

Choose a reason for hiding this comment

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

너무 좋아요! 기존의 에러 처리방식보다 확실히 더 좋아진 것 같아요!

Copy link
Contributor

Choose a reason for hiding this comment

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

useFetch가 useQuery, useMutation으로 대체되며 더 신경쓸 필요가 적어진 것 같아요 ㅎ.ㅎㅎㅎ

});

return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>;
};

export default QueryClientBoundary;
2 changes: 1 addition & 1 deletion client/src/components/Toast/Toast.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const Toast = ({
bottom = '0px',
isClickToClose = true,
position = 'bottom',
showingTime,
showingTime = 3000,
message,
onUndo,
onClose,
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Toast/Toast.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export interface ToastOptionProps {
onUndo?: () => void;
isClickToClose?: boolean;
onClose?: () => void;
showingTime: number;
showingTime?: number;
}

export interface ToastRequiredProps {
Expand Down
Loading