From a3a0fb80a68c241972a95bc417388362c8d3dd92 Mon Sep 17 00:00:00 2001 From: kimminna Date: Wed, 8 Jul 2026 02:09:37 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat(web):=20Suspense/ErrorBoundary=20?= =?UTF-8?q?=EA=B3=B5=ED=86=B5=20=EB=9E=98=ED=8D=BC=20=EC=B6=94=EA=B0=80=20?= =?UTF-8?q?(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit useSearchParams 등 클라이언트 훅 사용 시 필요한 Suspense 경계와, 향후 비동기 데이터 페칭 시 필요한 에러 처리를 공통 AsyncBoundary 컴포넌트로 추가했습니다. --- .../components/boundary/AsyncBoundary.tsx | 23 ++++++++++ .../components/boundary/ErrorBoundary.tsx | 46 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 apps/timo-web/components/boundary/AsyncBoundary.tsx create mode 100644 apps/timo-web/components/boundary/ErrorBoundary.tsx diff --git a/apps/timo-web/components/boundary/AsyncBoundary.tsx b/apps/timo-web/components/boundary/AsyncBoundary.tsx new file mode 100644 index 00000000..c2e303f1 --- /dev/null +++ b/apps/timo-web/components/boundary/AsyncBoundary.tsx @@ -0,0 +1,23 @@ +import { Suspense, type ReactNode } from "react"; + +import { ErrorBoundary } from "@/components/boundary/ErrorBoundary"; + +interface AsyncBoundaryProps { + children: ReactNode; + pendingFallback?: ReactNode; + errorFallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode); +} + +export const AsyncBoundary = ({ + children, + pendingFallback = null, + errorFallback, +}: AsyncBoundaryProps) => { + const content = {children}; + + if (!errorFallback) { + return content; + } + + return {content}; +}; diff --git a/apps/timo-web/components/boundary/ErrorBoundary.tsx b/apps/timo-web/components/boundary/ErrorBoundary.tsx new file mode 100644 index 00000000..968b3460 --- /dev/null +++ b/apps/timo-web/components/boundary/ErrorBoundary.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Component } from "react"; + +import type { ReactNode } from "react"; + +type ErrorFallback = + | ReactNode + | ((error: Error, reset: () => void) => ReactNode); + +interface ErrorBoundaryProps { + children: ReactNode; + fallback: ErrorFallback; +} + +interface ErrorBoundaryState { + error: Error | null; +} + +export class ErrorBoundary extends Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + state: ErrorBoundaryState = { error: null }; + + static getDerivedStateFromError(error: Error): ErrorBoundaryState { + return { error }; + } + + reset = (): void => { + this.setState({ error: null }); + }; + + render(): ReactNode { + const { error } = this.state; + const { children, fallback } = this.props; + + if (error) { + return typeof fallback === "function" + ? fallback(error, this.reset) + : fallback; + } + + return children; + } +} From d80c95d3b917cd0ca0e05fc5b26de52ab2d610b6 Mon Sep 17 00:00:00 2001 From: kimminna Date: Wed, 8 Jul 2026 17:00:52 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix(web):=20AsyncBoundary/ErrorBoundary=20?= =?UTF-8?q?=ED=83=80=EC=9E=85=20=EA=B3=B5=EC=9C=A0=20=EB=B0=8F=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20=EC=95=88=EC=A0=84=EC=84=B1=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20(#114)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ErrorFallbackTypes를 ErrorBoundary에서 export하여 AsyncBoundary가 재사용하도록 했습니다 - getDerivedStateFromError가 unknown 값을 안전하게 Error로 정규화하도록 수정했습니다 - componentDidCatch를 추가해 캐치된 에러를 Sentry로 전송하도록 했습니다 --- .../components/boundary/AsyncBoundary.tsx | 7 +++++-- .../components/boundary/ErrorBoundary.tsx | 20 ++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/apps/timo-web/components/boundary/AsyncBoundary.tsx b/apps/timo-web/components/boundary/AsyncBoundary.tsx index c2e303f1..07eb3111 100644 --- a/apps/timo-web/components/boundary/AsyncBoundary.tsx +++ b/apps/timo-web/components/boundary/AsyncBoundary.tsx @@ -1,11 +1,14 @@ import { Suspense, type ReactNode } from "react"; -import { ErrorBoundary } from "@/components/boundary/ErrorBoundary"; +import { + ErrorBoundary, + type ErrorFallbackTypes, +} from "@/components/boundary/ErrorBoundary"; interface AsyncBoundaryProps { children: ReactNode; pendingFallback?: ReactNode; - errorFallback?: ReactNode | ((error: Error, reset: () => void) => ReactNode); + errorFallback?: ErrorFallbackTypes; } export const AsyncBoundary = ({ diff --git a/apps/timo-web/components/boundary/ErrorBoundary.tsx b/apps/timo-web/components/boundary/ErrorBoundary.tsx index 968b3460..edf80bd9 100644 --- a/apps/timo-web/components/boundary/ErrorBoundary.tsx +++ b/apps/timo-web/components/boundary/ErrorBoundary.tsx @@ -1,30 +1,40 @@ "use client"; +import * as Sentry from "@sentry/nextjs"; import { Component } from "react"; -import type { ReactNode } from "react"; +import type { ErrorInfo, ReactNode } from "react"; -type ErrorFallback = +export type ErrorFallbackTypes = | ReactNode | ((error: Error, reset: () => void) => ReactNode); interface ErrorBoundaryProps { children: ReactNode; - fallback: ErrorFallback; + fallback: ErrorFallbackTypes; } interface ErrorBoundaryState { error: Error | null; } +const toError = (value: unknown): Error => + value instanceof Error ? value : new Error(String(value)); + export class ErrorBoundary extends Component< ErrorBoundaryProps, ErrorBoundaryState > { state: ErrorBoundaryState = { error: null }; - static getDerivedStateFromError(error: Error): ErrorBoundaryState { - return { error }; + static getDerivedStateFromError(error: unknown): ErrorBoundaryState { + return { error: toError(error) }; + } + + componentDidCatch(error: unknown, errorInfo: ErrorInfo): void { + Sentry.captureException(toError(error), { + contexts: { react: { componentStack: errorInfo.componentStack } }, + }); } reset = (): void => {