FE Debugging & Error Handling
$ARGUMENTS 로 전달된 에러 메시지 또는 파일을 분석하여 원인을 진단하고 해결책을 제시한다.
진단 절차
-
에러 파악: 에러 메시지, 스택 트레이스, 재현 조건을 확인한다
-
원인 분석: 코드를 읽고 에러 패턴 목록에서 해당하는 항목을 식별한다
-
해결책 제시: 구체적인 수정 코드와 함께 원인을 설명한다
-
재발 방지: Error Boundary, 타입 강화 등 예방 조치를 안내한다
React 일반 에러
Hydration Mismatch
에러: Text content does not match server-rendered HTML
원인: 서버와 클라이언트에서 렌더링 결과가 다름
// Bad — 서버/클라이언트 불일치 function Timestamp() { return <p>{new Date().toLocaleString()}</p>; }
// Good — 클라이언트에서만 렌더링 function Timestamp() { const [mounted, setMounted] = useState(false); useEffect(() => setMounted(true), []);
if (!mounted) return <p>Loading...</p>; return <p>{new Date().toLocaleString()}</p>; }
// Best — suppressHydrationWarning (단순 케이스) function Timestamp() { return <p suppressHydrationWarning>{new Date().toLocaleString()}</p>; }
주요 원인:
-
Date , Math.random() 등 비결정적 값
-
window , localStorage 등 브라우저 API 접근
-
브라우저 확장 프로그램이 DOM을 수정
-
잘못된 HTML 중첩 (<p> 안에 <div> 등)
Too Many Re-renders
에러: Too many re-renders. React limits the number of renders to prevent an infinite loop.
// Bad — 렌더링 중 setState 직접 호출 function Counter() { const [count, setCount] = useState(0); setCount(count + 1); // 무한 루프! return <p>{count}</p>; }
// Bad — 이벤트 핸들러에서 함수 호출 결과를 전달 <button onClick={setCount(count + 1)}>+</button>
// Good — 함수 참조를 전달 <button onClick={() => setCount(count + 1)}>+</button>
Cannot Update During Render
에러: Cannot update a component while rendering a different component
// Bad — 자식 렌더링 중 부모 상태 변경 function Child({ onUpdate }: { onUpdate: (v: string) => void }) { onUpdate("value"); // 렌더링 중 호출! return <div />; }
// Good — useEffect로 감싸기 function Child({ onUpdate }: { onUpdate: (v: string) => void }) { useEffect(() => { onUpdate("value"); }, [onUpdate]); return <div />; }
Memory Leak Warning
에러: Can't perform a React state update on an unmounted component
// Bad — 마운트 해제 후 setState function UserProfile({ id }: { id: string }) { const [user, setUser] = useState(null);
useEffect(() => { fetchUser(id).then(setUser); // 컴포넌트가 이미 언마운트될 수 있음 }, [id]); }
// Good — AbortController로 요청 취소 function UserProfile({ id }: { id: string }) { const [user, setUser] = useState(null);
useEffect(() => { const controller = new AbortController(); fetchUser(id, { signal: controller.signal }).then(setUser).catch(() => {}); return () => controller.abort(); }, [id]); }
// Best — TanStack Query 사용 (자동 취소) function UserProfile({ id }: { id: string }) { const { data: user } = useQuery({ queryKey: ["user", id], queryFn: () => fetchUser(id), }); }
Next.js 특유 에러
"use client" 관련
// Error: useState only works in Client Components // 원인: Server Component에서 훅 사용 // 해결: 파일 최상단에 "use client" 추가
// Error: async/await is not yet supported in Client Components // 원인: Client Component를 async로 선언 // 해결: 데이터 페칭을 Server Component로 이동하거나 TanStack Query 사용
Dynamic Import 에러
// Error: Element type is invalid // 원인: dynamic import에서 named export를 default로 접근
// Bad const Chart = dynamic(() => import("recharts"));
// Good — named export 명시 const Chart = dynamic(() => import("recharts").then((mod) => ({ default: mod.LineChart })) );
Server/Client 경계 에러
// Error: Functions cannot be passed directly to Client Components unless you explicitly expose it by marking it with "use server"
// Bad — 서버 함수를 Client Component에 직접 전달 async function Page() { async function getData() { /* ... */ } return <ClientComponent getData={getData} />; }
// Good — Server Action으로 표시 async function Page() { async function getData() { "use server"; /* ... */ } return <ClientComponent getData={getData} />; }
Error Boundary 패턴
기본 Error Boundary
// src/components/ErrorBoundary.tsx "use client";
import { Component, type ErrorInfo, type ReactNode } from "react"; import { Button } from "@/components/ui/button";
interface Props { children: ReactNode; fallback?: ReactNode; }
interface State { hasError: boolean; error: Error | null; }
class ErrorBoundary extends Component<Props, State> { state: State = { hasError: false, error: null };
static getDerivedStateFromError(error: Error): State { return { hasError: true, error }; }
componentDidCatch(error: Error, errorInfo: ErrorInfo) { console.error("ErrorBoundary caught:", error, errorInfo); }
render() { if (this.state.hasError) { return ( this.props.fallback ?? ( <div className="flex flex-col items-center gap-4 p-8"> <h2 className="text-lg font-semibold">문제가 발생했습니다</h2> <p className="text-muted-foreground"> {this.state.error?.message} </p> <Button onClick={() => this.setState({ hasError: false, error: null })} > 다시 시도 </Button> </div> ) ); }
return this.props.children;
} }
export { ErrorBoundary };
Next.js error.tsx (App Router)
// src/app/error.tsx "use client";
import { Button } from "@/components/ui/button";
interface ErrorPageProps { error: Error & { digest?: string }; reset: () => void; }
export default function ErrorPage({ error, reset }: ErrorPageProps) { return ( <div className="flex min-h-[400px] flex-col items-center justify-center gap-4"> <h2 className="text-xl font-semibold">문제가 발생했습니다</h2> <p className="text-muted-foreground">{error.message}</p> <Button onClick={reset}>다시 시도</Button> </div> ); }
라우트 그룹별 에러 처리
src/app/ ├── error.tsx # 전역 에러 페이지 ├── not-found.tsx # 404 페이지 ├── (auth)/ │ ├── error.tsx # 인증 관련 에러 │ └── login/page.tsx ├── (dashboard)/ │ ├── error.tsx # 대시보드 에러 │ └── dashboard/page.tsx └── global-error.tsx # Root Layout 에러 (layout.tsx 에러 캐치)
디버깅 기법
React DevTools 활용
// 컴포넌트에 displayName 설정 (DevTools에서 식별) const MemoizedComponent = memo(function ProductCard({ product }: Props) { return <div>{product.name}</div>; });
// Profiler로 렌더링 성능 측정 import { Profiler } from "react";
<Profiler
id="ProductList"
onRender={(id, phase, actualDuration) => {
console.log(${id} ${phase}: ${actualDuration}ms);
}}
<ProductList /> </Profiler>
조건부 디버깅
// 개발 환경에서만 로깅 if (process.env.NODE_ENV === "development") { console.log("Debug:", data); }
// useEffect 디버깅 — 어떤 의존성이 변경되었는지 추적 function useWhyDidYouUpdate(name: string, props: Record<string, unknown>) { const previousProps = useRef(props);
useEffect(() => { const allKeys = Object.keys({ ...previousProps.current, ...props }); const changes: Record<string, { from: unknown; to: unknown }> = {};
for (const key of allKeys) {
if (previousProps.current[key] !== props[key]) {
changes[key] = { from: previousProps.current[key], to: props[key] };
}
}
if (Object.keys(changes).length > 0) {
console.log(`[${name}] changed:`, changes);
}
previousProps.current = props;
}); }
TanStack Query 디버깅
// 개발 환경에서 DevTools 활성화 import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
function Providers({ children }: { children: React.ReactNode }) { return ( <QueryClientProvider client={queryClient}> {children} <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> ); }
일반적인 TypeScript 에러
Type Narrowing
// Error: Object is possibly 'undefined' // 해결: optional chaining 또는 type guard
// nullable 체크 function getUserName(user: User | null) { return user?.name ?? "Unknown"; }
// 배열 접근 const first = items[0]; // string | undefined if (first !== undefined) { console.log(first.toUpperCase()); // OK }
// discriminated union type Result = { success: true; data: User } | { success: false; error: string };
function handleResult(result: Result) { if (result.success) { console.log(result.data); // User 타입으로 좁혀짐 } else { console.log(result.error); // string 타입으로 좁혀짐 } }
일반적인 TS 실수
// Error: Type 'string' is not assignable to type '"a" | "b"' const value: string = "a"; // Bad const result: "a" | "b" = value; // Good const result: "a" | "b" = value as "a" | "b"; // 확실할 때만 // Best — 유효성 검사 후 사용 function isValidValue(v: string): v is "a" | "b" { return v === "a" || v === "b"; } if (isValidValue(value)) { const result: "a" | "b" = value; // OK }
리포트 형식
Debug Report: [에러 메시지/파일명]
에러 요약
- 에러 타입: [Runtime / Type / Build / Hydration]
- 에러 메시지:
... - 발생 위치:
파일:라인
원인 분석
설명...
해결책
즉시 수정
```tsx // before ... // after ... ```
재발 방지
- Error Boundary 추가
- 타입 강화
- 테스트 추가
실행 규칙
-
에러 메시지가 전달되면 패턴 매칭으로 빠르게 원인을 식별한다
-
파일 경로가 전달되면 코드를 읽고 잠재적 에러 포인트를 분석한다
-
스택 트레이스가 있으면 관련 파일을 추적하여 읽는다
-
해결책에는 항상 before/after 코드를 포함한다
-
에러 재현이 어려운 경우 디버깅 기법을 안내한다
-
Error Boundary, 타입 강화 등 예방 조치를 함께 제안한다