-
Notifications
You must be signed in to change notification settings - Fork 3
[Feature] 전역 에러 처리 및 Sentry 연동 #1196
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
Changes from all commits
b307f53
183fcb9
7b7c379
18f6af2
41c7222
3360d24
0efa1a6
6a5d8a2
0a7e873
26e885f
4ddd46a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { ReactNode, Suspense } from 'react'; | ||
| import * as Sentry from '@sentry/react'; | ||
| import Spinner from '../Spinner/Spinner'; | ||
| import GlobalErrorFallback from './GlobalErrorFallback'; | ||
|
|
||
| interface GlobalBoundaryProps { | ||
| children: ReactNode; | ||
| } | ||
|
|
||
| const GlobalBoundary = ({ children }: GlobalBoundaryProps) => { | ||
| return ( | ||
| <Sentry.ErrorBoundary | ||
| fallback={(errorData) => ( | ||
| <GlobalErrorFallback | ||
| error={errorData.error as Error} | ||
| resetError={errorData.resetError} | ||
| /> | ||
| )} | ||
| > | ||
seongwon030 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| <Suspense fallback={<Spinner />}>{children}</Suspense> | ||
| </Sentry.ErrorBoundary> | ||
|
Comment on lines
+20
to
+21
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suspense가 GlobalBoundary로 이동하면서 로딩과 에러 처리가 한 곳에서 관리되도록 정리된 것 같네요
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. useQuery를 쓰고 있는 페이지에선 내부에서 로딩,에러 상태를 관리하니 suspense 대상이 아니지만 안전빵이라도 Suspense보단 에러 추적 목적으로 상위 에러바운더리로 전파하는 방식이 좋을 것 같아요. |
||
| ); | ||
| }; | ||
|
|
||
| export default GlobalBoundary; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| import styled from 'styled-components'; | ||
|
|
||
| export const Container = styled.div` | ||
| min-height: 100vh; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| padding: 20px; | ||
| background: linear-gradient(135deg, #fff5f0 0%, #ffffff 100%); | ||
| `; | ||
|
|
||
| export const Content = styled.div` | ||
| max-width: 600px; | ||
| width: 100%; | ||
| text-align: center; | ||
| background: white; | ||
| border-radius: 16px; | ||
| padding: 48px 32px; | ||
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); | ||
| `; | ||
|
|
||
| export const IconWrapper = styled.div` | ||
| margin-bottom: 24px; | ||
| color: #ff5414; | ||
| display: flex; | ||
| justify-content: center; | ||
|
|
||
| svg { | ||
| width: 64px; | ||
| height: 64px; | ||
| } | ||
| `; | ||
|
|
||
| export const Title = styled.h1` | ||
| font-size: 24px; | ||
| font-weight: 700; | ||
| color: #111111; | ||
| margin-bottom: 16px; | ||
| line-height: 1.4; | ||
| `; | ||
|
|
||
| export const Message = styled.p` | ||
| font-size: 16px; | ||
| font-weight: 500; | ||
| color: #787878; | ||
| line-height: 1.6; | ||
| margin-bottom: 32px; | ||
| `; | ||
|
|
||
| export const ErrorDetails = styled.div` | ||
| background: #f5f5f5; | ||
| border: 1px solid #ebebeb; | ||
| border-radius: 8px; | ||
| padding: 16px; | ||
| margin-bottom: 32px; | ||
| text-align: left; | ||
| max-height: 300px; | ||
| overflow-y: auto; | ||
| `; | ||
|
|
||
| export const ErrorDetailsTitle = styled.div` | ||
| font-size: 12px; | ||
| font-weight: 600; | ||
| color: #989898; | ||
| margin-bottom: 12px; | ||
| text-transform: uppercase; | ||
| letter-spacing: 0.5px; | ||
| `; | ||
|
|
||
| export const ErrorMessage = styled.div` | ||
| font-size: 14px; | ||
| font-weight: 600; | ||
| color: #ff5414; | ||
| margin-bottom: 12px; | ||
| word-break: break-word; | ||
| `; | ||
|
|
||
| export const StackTrace = styled.pre` | ||
| font-size: 12px; | ||
| font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; | ||
| color: #4b4b4b; | ||
| white-space: pre-wrap; | ||
| word-break: break-all; | ||
| line-height: 1.5; | ||
| `; | ||
|
|
||
| export const ButtonGroup = styled.div` | ||
| display: flex; | ||
| gap: 12px; | ||
| justify-content: center; | ||
| flex-wrap: wrap; | ||
| `; | ||
|
|
||
| const BaseButton = styled.button` | ||
| padding: 14px 32px; | ||
| font-size: 16px; | ||
| font-weight: 600; | ||
| border-radius: 8px; | ||
| border: none; | ||
| cursor: pointer; | ||
| transition: all 0.2s ease; | ||
| min-width: 140px; | ||
| font-family: 'Pretendard', sans-serif; | ||
|
|
||
| &:active { | ||
| transform: scale(0.98); | ||
| } | ||
| `; | ||
|
|
||
| export const PrimaryButton = styled(BaseButton)` | ||
| background: #ff5414; | ||
| color: white; | ||
|
|
||
| &:hover { | ||
| background: #ff7543; | ||
| box-shadow: 0 4px 12px rgba(255, 84, 20, 0.3); | ||
| } | ||
| `; | ||
|
|
||
| export const SecondaryButton = styled(BaseButton)` | ||
| background: white; | ||
| color: #4b4b4b; | ||
| border: 1px solid #dcdcdc; | ||
|
|
||
| &:hover { | ||
| background: #f5f5f5; | ||
| border-color: #c5c5c5; | ||
| } | ||
| `; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import * as Styled from './GlobalErrorFallback.styles'; | ||
|
|
||
| interface ErrorFallbackProps { | ||
| error: Error; | ||
| resetError: () => void; | ||
| } | ||
|
|
||
| const WarningIcon = () => ( | ||
| <svg | ||
| viewBox='0 0 24 24' | ||
| fill='none' | ||
| xmlns='http://www.w3.org/2000/svg' | ||
| stroke='currentColor' | ||
| > | ||
| <path | ||
| d='M12 9V14M12 17.5V18M12 22C6.477 22 2 17.523 2 12C2 6.477 6.477 2 12 2C17.523 2 22 6.477 22 12C22 17.523 17.523 22 12 22Z' | ||
| strokeWidth='2' | ||
| strokeLinecap='round' | ||
| strokeLinejoin='round' | ||
| /> | ||
| </svg> | ||
| ); | ||
|
|
||
| const GlobalErrorFallback = ({ error, resetError }: ErrorFallbackProps) => { | ||
| const isDev = import.meta.env.DEV; | ||
|
|
||
| const handleReload = () => { | ||
| window.location.href = '/'; | ||
| }; | ||
|
|
||
| const handleReset = () => { | ||
| resetError(); | ||
| }; | ||
|
|
||
| return ( | ||
| <Styled.Container> | ||
| <Styled.Content> | ||
| <Styled.IconWrapper> | ||
| <WarningIcon /> | ||
| </Styled.IconWrapper> | ||
|
|
||
| <Styled.Title>서비스 이용에 불편을 드려 죄송합니다</Styled.Title> | ||
| <Styled.Message> | ||
| 예상치 못한 오류가 발생하여 페이지를 표시할 수 없습니다. | ||
| <br /> | ||
| 잠시 후 다시 시도해 주세요. | ||
| </Styled.Message> | ||
|
|
||
| {isDev && error && ( | ||
| <Styled.ErrorDetails> | ||
| <Styled.ErrorDetailsTitle> | ||
| 개발자 정보 (프로덕션에서는 표시되지 않습니다) | ||
| </Styled.ErrorDetailsTitle> | ||
| <Styled.ErrorMessage>{error.message}</Styled.ErrorMessage> | ||
| {error.stack && ( | ||
| <Styled.StackTrace>{error.stack}</Styled.StackTrace> | ||
| )} | ||
| </Styled.ErrorDetails> | ||
| )} | ||
seongwon030 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| <Styled.ButtonGroup> | ||
| <Styled.PrimaryButton onClick={handleReset}> | ||
| 다시 시도 | ||
| </Styled.PrimaryButton> | ||
|
Comment on lines
+62
to
+64
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. handleReset이 Sentry에서 전달해주는 resetError이라는 함수군요
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 네 Sentry 에러바운더리 내에서 에러 상태를 초기화하는 방식입니다 ! |
||
| <Styled.SecondaryButton onClick={handleReload}> | ||
| 홈으로 이동 | ||
| </Styled.SecondaryButton> | ||
| </Styled.ButtonGroup> | ||
| </Styled.Content> | ||
| </Styled.Container> | ||
| ); | ||
| }; | ||
|
|
||
| export default GlobalErrorFallback; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export * from './GlobalErrorFallback'; | ||
| export { default as GlobalBoundary } from './GlobalBoundary'; |
Uh oh!
There was an error while loading. Please reload this page.