Skip to content

Commit

Permalink
feat: react-query에 맞는 error handling 적용 (#415)
Browse files Browse the repository at this point in the history
* chore: 디자인시스템 버전 적용

* rename: 파일 이름 변경

* fix: 사용하지 않는 Error 처리 로직 삭제

* feat: 전역 에러 상태 생성

* fix: 바뀐 파일 이름 적용

* fix: ToastProvider 내에서 에러 처리 로직 제거

* refactor: AppErrorBoundary 및 QueryClientBoundary를 통한 에러 처리

* fix: 사용하지 않는 코드 임시적으로 주석 처리
TODO: 테스트 코드 역할 위임

* mover: AppErrorBoundary, QueryClientBoundary 코드 위치 변경

* fix: test 코드 변경

* chore: jest path alias 적용

* test: AppErrorBoundary 테스트코드 작성

* remove: 사용하지 않는 코드 삭제

* fix: 사용하지 않는 코드 주석처리

* fix: AppErrorBoundary, QueryClientBoundary 로직 수정

* test: AppErrorBoundary test코드 작성

* style: lint 적용

* feat: App에 ErrorBoundary 추가

* feat: UnhandledErrorBoundary 컴포넌트 구현

* feat: 에러를 구독하며 핸들링되는 에러면 토스트, 핸들링 불가능한 에러면 에러 바운더리를 띄우도록 하는 캐처 구현

* feat: 에러 페이지에 메일 추가

* feat: errorInfo를 안에서 구현하는 것이 아닌 구현된 것을 인자로 받도록 수정

* chore: 파일 위치 수정에 따라 import 경로 수정

* chore: 불필요해진 파일 제거

* chore: 개행 추가

* feat: ErrorCatcher에 대한 테스트 코드 작성

* feat: Toast의 showingTime 을 옵셔널로 수정

* chore: 없어진 컴포넌트를 renderHook에서 제거

* refactor: return문이 반복되는 부분을 리펙터링

* feat: useToast에 대한 테스트코드 작성

* chore: 사용하지 않는 파일 제거

* chore: 불필요한 파일이 커버리지에 뜨지 않도록 ignore에 추가

* rename: 파일명 오타 수정

* chore: import 경로 수정

* chore: 병힙

* chore: 라이브러리 오류로 인해 테스트 통과가 안되므로 디자인 시스템 라이브러리 다운그레이드

---------

Co-authored-by: pakxe <[email protected]>
  • Loading branch information
Todari and pakxe authored Aug 22, 2024
1 parent 56ba99b commit f76dbdf
Show file tree
Hide file tree
Showing 31 changed files with 319 additions and 897 deletions.
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
26 changes: 4 additions & 22 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
"@emotion/react": "^11.11.4",
"@sentry/react": "^8.25.0",
"@tanstack/react-query": "^5.51.23",
"haengdong-design": "^0.1.80",
"haengdong-design": "^0.1.79",
"react": "^18.3.1",
"react-copy-to-clipboard": "^5.1.0",
"react-dom": "^18.3.1",
Expand Down
34 changes: 12 additions & 22 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,29 @@
import {Outlet} from 'react-router-dom';
import {HDesignProvider} from 'haengdong-design';
import {Global} from '@emotion/react';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';

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 {GlobalStyle} from './GlobalStyle';
import UnhandledErrorBoundary from './UnhandledErrorBoundary';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60, // 1 minute
gcTime: 1000 * 60, // 1 minute
},
},
});

const App: React.FC = () => {
return (
<HDesignProvider>
<QueryClientProvider client={queryClient}>
<ReactQueryDevtools initialIsOpen={false} />
<UnhandledErrorBoundary>
<Global styles={GlobalStyle} />
<ErrorProvider>
{/* <ErrorProvider callback={toast}> */}
<ToastProvider>
<UnhandledErrorBoundary>
<Global styles={GlobalStyle} />
<ToastProvider>
<ErrorCatcher>
<QueryClientBoundary>
<ReactQueryDevtools initialIsOpen={false} />
<Outlet />
</ToastProvider>
</ErrorProvider>
</UnhandledErrorBoundary>
</QueryClientProvider>
</QueryClientBoundary>
</ErrorCatcher>
</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 '../../UnhandledErrorBoundary';

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

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

0 comments on commit f76dbdf

Please sign in to comment.