fe-debug
SKILL.md
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, 타입 강화 등 예방 조치를 함께 제안한다
Weekly Installs
4
Repository
ingpdw/pdw-fe-dev-toolFirst Seen
Feb 7, 2026
Security Audits
Installed on
gemini-cli4
github-copilot4
codex4
cursor4
opencode4
openclaw3