fe-debug

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

FE Debugging & Error Handling

前端调试与错误处理

$ARGUMENTS
로 전달된 에러 메시지 또는 파일을 분석하여 원인을 진단하고 해결책을 제시한다.
通过分析
$ARGUMENTS
传递的错误消息或文件,诊断问题原因并提供解决方案。

진단 절차

诊断流程

  1. 에러 파악: 에러 메시지, 스택 트레이스, 재현 조건을 확인한다
  2. 원인 분석: 코드를 읽고 에러 패턴 목록에서 해당하는 항목을 식별한다
  3. 해결책 제시: 구체적인 수정 코드와 함께 원인을 설명한다
  4. 재발 방지: Error Boundary, 타입 강화 등 예방 조치를 안내한다
  1. 错误确认:检查错误消息、堆栈跟踪、复现条件
  2. 原因分析:阅读代码并从错误模式列表中识别对应项
  3. 解决方案提供:结合具体修改代码说明问题原因
  4. 防止复发:指导添加Error Boundary、强化类型等预防措施

React 일반 에러

React常见错误

Hydration Mismatch

Hydration Mismatch(水合不匹配)

에러:
Text content does not match server-rendered HTML
원인: 서버와 클라이언트에서 렌더링 결과가 다름
tsx
// 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>
    등)
错误
Text content does not match server-rendered HTML
原因:服务端与客户端渲染结果不一致
tsx
// 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(重渲染次数过多)

에러:
Too many re-renders. React limits the number of renders to prevent an infinite loop.
tsx
// 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>
错误
Too many re-renders. React limits the number of renders to prevent an infinite loop.
tsx
// 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 During Render(渲染期间无法更新)

에러:
Cannot update a component while rendering a different component
tsx
// 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 />;
}
错误
Cannot update a component while rendering a different component
tsx
// 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

Memory Leak Warning(内存泄漏警告)

에러:
Can't perform a React state update on an unmounted component
tsx
// 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),
  });
}
错误
Can't perform a React state update on an unmounted component
tsx
// 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 특유 에러

Next.js特有错误

"use client" 관련

"use client"相关

tsx
// 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 사용
tsx
// 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 에러

Dynamic Import错误

tsx
// 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 }))
);
tsx
// Error: Element type is invalid
// 原因:在动态导入中把命名导出当作默认导出访问

// Bad
const Chart = dynamic(() => import("recharts"));

// Good — 明确指定命名导出
const Chart = dynamic(() =>
  import("recharts").then((mod) => ({ default: mod.LineChart }))
);

Server/Client 경계 에러

Server/Client边界错误

tsx
// 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} />;
}
tsx
// 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模式

기본 Error Boundary

基础Error Boundary

tsx
// 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 };
tsx
// 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)

Next.js error.tsx(App Router)

tsx
// 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>
  );
}
tsx
// 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 에러 캐치)
src/app/
├── error.tsx              # 全局错误页面
├── not-found.tsx          # 404页面
├── (auth)/
│   ├── error.tsx          # 认证相关错误
│   └── login/page.tsx
├── (dashboard)/
│   ├── error.tsx          # 仪表盘错误
│   └── dashboard/page.tsx
└── global-error.tsx       # 根布局错误(捕获layout.tsx错误)

디버깅 기법

调试技巧

React DevTools 활용

React DevTools的使用

tsx
// 컴포넌트에 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>
tsx
// 为组件设置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>

조건부 디버깅

条件式调试

typescript
// 개발 환경에서만 로깅
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;
  });
}
typescript
// 仅在开发环境中记录日志
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 디버깅

TanStack Query调试

tsx
// 개발 환경에서 DevTools 활성화
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}
tsx
// 在开发环境中启用DevTools
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

일반적인 TypeScript 에러

常见TypeScript错误

Type Narrowing

Type Narrowing(类型收窄)

typescript
// 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 타입으로 좁혀짐
  }
}
typescript
// Error: Object is possibly 'undefined'
// 解决:可选链或类型守卫

// 可空值检查
function getUserName(user: User | null) {
  return user?.name ?? "Unknown";
}

// 数组访问
const first = items[0]; // string | undefined
if (first !== undefined) {
  console.log(first.toUpperCase()); // OK
}

// 可辨识联合
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 실수

常见TS错误

typescript
// 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
}
typescript
// 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
}

리포트 형식

报告格式

markdown
undefined
markdown
undefined

Debug Report: [에러 메시지/파일명]

调试报告: [错误消息/文件名]

에러 요약

错误摘要

  • 에러 타입: [Runtime / Type / Build / Hydration]
  • 에러 메시지:
    ...
  • 발생 위치:
    파일:라인
  • 错误类型: [运行时/类型/构建/水合]
  • 错误消息:
    ...
  • 发生位置:
    文件:行

원인 분석

原因分析

설명...
说明...

해결책

解决方案

즉시 수정

即时修复

```tsx // before ... // after ... ```
```tsx // before ... // after ... ```

재발 방지

防止复发

  • Error Boundary 추가
  • 타입 강화
  • 테스트 추가
undefined
  • 添加Error Boundary
  • 强化类型
  • 添加测试
undefined

실행 규칙

执行规则

  1. 에러 메시지가 전달되면 패턴 매칭으로 빠르게 원인을 식별한다
  2. 파일 경로가 전달되면 코드를 읽고 잠재적 에러 포인트를 분석한다
  3. 스택 트레이스가 있으면 관련 파일을 추적하여 읽는다
  4. 해결책에는 항상 before/after 코드를 포함한다
  5. 에러 재현이 어려운 경우 디버깅 기법을 안내한다
  6. Error Boundary, 타입 강화 등 예방 조치를 함께 제안한다
  1. 收到错误消息时,通过模式匹配快速识别原因
  2. 收到文件路径时,读取代码并分析潜在错误点
  3. 如有堆栈跟踪,追踪并读取相关文件
  4. 解决方案中始终包含before/after代码
  5. 若错误难以复现,指导使用调试技巧
  6. 同时建议添加Error Boundary、强化类型等预防措施