-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: react-query에 맞는 error handling 적용 (#415)
* 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
Showing
31 changed files
with
319 additions
and
897 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
92 changes: 92 additions & 0 deletions
92
client/src/components/AppErrorBoundary/ErrorCatcher.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
24
client/src/components/QueryClientBoundary/QueryClientBoundary.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.