fe-debug

📁 ingpdw/pdw-fe-dev-tool 📅 6 days ago
1
总安装量
1
周安装量
#43645
全站排名
安装命令
npx skills add https://github.com/ingpdw/pdw-fe-dev-tool --skill fe-debug

Agent 安装分布

mcpjam 1
claude-code 1
replit 1
junie 1
zencoder 1

Skill 文档

FE Debugging & Error Handling

$ARGUMENTS로 전달된 에러 메시지 또는 파일을 분석하여 원인을 진단하고 해결책을 제시한다.

진단 절차

  1. 에러 파악: 에러 메시지, 스택 트레이스, 재현 조건을 확인한다
  2. 원인 분석: 코드를 읽고 에러 패턴 목록에서 해당하는 항목을 식별한다
  3. 해결책 제시: 구체적인 수정 코드와 함께 원인을 설명한다
  4. 재발 방지: 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 추가
- 타입 강화
- 테스트 추가

실행 규칙

  1. 에러 메시지가 전달되면 패턴 매칭으로 빠르게 원인을 식별한다
  2. 파일 경로가 전달되면 코드를 읽고 잠재적 에러 포인트를 분석한다
  3. 스택 트레이스가 있으면 관련 파일을 추적하여 읽는다
  4. 해결책에는 항상 before/after 코드를 포함한다
  5. 에러 재현이 어려운 경우 디버깅 기법을 안내한다
  6. Error Boundary, 타입 강화 등 예방 조치를 함께 제안한다