Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 src/app/router/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ import {
PublicRoute,
} from "@/shared/router/components/RouteGuards";
import { RootRoute } from "@/shared/router/RootRoute";
import { RouteErrorBoundary } from "@/shared/router/RouteErrorBoundary";

export const router = createBrowserRouter([
{
element: <RootRoute />,
// 라우트 트리 내부에서 throw된 렌더/로더 에러를 잡아 500 페이지로 폴백
errorElement: <RouteErrorBoundary />,
children: [
// ========================================
// 🌐 Public Routes (인증 지향적이나 로그인이 필수는 아님)
Expand Down
12 changes: 8 additions & 4 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { RouterProvider } from "react-router-dom";
import { AppQueryProvider } from "./app/providers/query_provider";
import { router } from "./app/router/routes";
import { ToastHost } from "@/shared/components/ui/ToastHost";
import { ErrorBoundary } from "@/shared/components/ErrorBoundary";
import { RootErrorFallback } from "@/shared/components/RootErrorFallback";
import "./app/styles/global.css";

const enableMocking = async () => {
Expand All @@ -20,10 +22,12 @@ const enableMocking = async () => {
enableMocking().then(() => {
createRoot(document.getElementById("root")!).render(
<StrictMode>
<AppQueryProvider>
<RouterProvider router={router} />
<ToastHost />
</AppQueryProvider>
<ErrorBoundary fallback={<RootErrorFallback />}>
<AppQueryProvider>
<RouterProvider router={router} />
<ToastHost />
</AppQueryProvider>
</ErrorBoundary>
</StrictMode>,
);
});
50 changes: 50 additions & 0 deletions src/shared/components/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Component, type ErrorInfo, type ReactNode } from "react";

interface ErrorBoundaryProps {
children: ReactNode;
/** 에러 발생 시 보여줄 폴백. reset 콜백으로 경계 상태를 초기화할 수 있다. */
fallback: ReactNode | ((error: Error, reset: () => void) => ReactNode);
/** 외부 로깅(Sentry 등) 연동 지점 */
onError?: (error: Error, info: ErrorInfo) => void;
}

interface ErrorBoundaryState {
error: Error | null;
}

/**
* 렌더 단계에서 throw된 예외를 잡아 흰 화면(white screen) 대신 폴백 UI를 보여준다.
*
* 주의: 이벤트 핸들러/비동기 코드의 예외는 React 특성상 잡지 못한다(클래스 경계 공통).
*/
export class ErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
state: ErrorBoundaryState = { error: null };

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { error };
}

componentDidCatch(error: Error, info: ErrorInfo) {
// TODO: 추후 Sentry 등 외부 에러 로깅 연동 지점
console.error("[ErrorBoundary]", error, info.componentStack);
this.props.onError?.(error, info);
}

reset = () => this.setState({ error: null });

render() {
const { error } = this.state;
const { fallback, children } = this.props;

if (error) {
return typeof fallback === "function"
? fallback(error, this.reset)
: fallback;
}

return children;
}
}
34 changes: 34 additions & 0 deletions src/shared/components/RootErrorFallback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { ProovyIcon } from "@/shared/components/icons/ProovyIcon";

/**
* 최상단 ErrorBoundary 폴백.
*
* 라우터 바깥(provider 등)에서 터진 catastrophic 에러용이라
* 라우터 컨텍스트가 없을 수 있어 SPA 네비게이션 대신 window.location을 사용한다.
*/
export const RootErrorFallback = () => {
return (
<div className="flex min-h-screen w-full items-center justify-center bg-white">
<div className="flex flex-col items-center">
<ProovyIcon className="h-40 w-40" />

<p className="mt-12 font-[Pretendard] text-[32px] font-semibold text-black">
문제가 발생했습니다
</p>
<p className="mt-4 max-w-[420px] text-center font-[Pretendard] text-[16px] leading-[26px] text-black">
예기치 못한 오류로 화면을 표시할 수 없습니다.
<br />
페이지를 새로고침해 주세요.
</p>

<button
type="button"
onClick={() => window.location.reload()}
className="mt-12 flex h-[52px] w-[280px] items-center justify-center rounded-xl border-[0.5px] border-[#D1D6DE] bg-white font-[Pretendard] text-[20px] font-semibold text-black transition-all duration-300 hover:border-transparent hover:text-[#2A6AFF] hover:shadow-[0px_4px_40px_0px_rgba(0,0,0,0.25)] active:border-transparent active:bg-[#2A6AFF] active:text-white"
>
새로고침
</button>
</div>
</div>
);
};
20 changes: 20 additions & 0 deletions src/shared/router/RouteErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { useEffect } from "react";
import { useRouteError } from "react-router-dom";
import { ServerErrorPage } from "@/pages/error/ServerErrorPage";

/**
* 라우트 트리 내부에서 throw된 렌더/로더 에러를 잡는 경계.
*
* 라우터 컨텍스트 안에서 렌더되므로 기존 500 에러 페이지를 그대로 재활용한다.
* (errorElement로 연결 → useNavigate 등 라우터 훅 정상 동작)
*/
export const RouteErrorBoundary = () => {
const error = useRouteError();

useEffect(() => {
// TODO: 추후 Sentry 등 외부 에러 로깅 연동 지점
console.error("[RouteError]", error);
}, [error]);

return <ServerErrorPage />;
};