fe-debug
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFE Debugging & Error Handling
前端调试与错误处理
$ARGUMENTS通过分析传递的错误消息或文件,诊断问题原因并提供解决方案。
$ARGUMENTS진단 절차
诊断流程
- 에러 파악: 에러 메시지, 스택 트레이스, 재현 조건을 확인한다
- 원인 분석: 코드를 읽고 에러 패턴 목록에서 해당하는 항목을 식별한다
- 해결책 제시: 구체적인 수정 코드와 함께 원인을 설명한다
- 재발 방지: Error Boundary, 타입 강화 등 예방 조치를 안내한다
- 错误确认:检查错误消息、堆栈跟踪、复现条件
- 原因分析:阅读代码并从错误模式列表中识别对应项
- 解决方案提供:结合具体修改代码说明问题原因
- 防止复发:指导添加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등 브라우저 API 접근localStorage - 브라우저 확장 프로그램이 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等浏览器APIlocalStorage - 浏览器扩展程序修改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 componenttsx
// 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 componenttsx
// 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 componenttsx
// 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 componenttsx
// 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 QueryDynamic 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
undefinedmarkdown
undefinedDebug Report: [에러 메시지/파일명]
调试报告: [错误消息/文件名]
에러 요약
错误摘要
- 에러 타입: [Runtime / Type / Build / Hydration]
- 에러 메시지:
... - 발생 위치:
파일:라인
- 错误类型: [运行时/类型/构建/水合]
- 错误消息:
... - 发生位置:
文件:行
원인 분석
原因分析
설명...
说明...
해결책
解决方案
즉시 수정
即时修复
```tsx
// before
...
// after
...
```
```tsx
// before
...
// after
...
```
재발 방지
防止复发
- Error Boundary 추가
- 타입 강화
- 테스트 추가
undefined- 添加Error Boundary
- 强化类型
- 添加测试
undefined실행 규칙
执行规则
- 에러 메시지가 전달되면 패턴 매칭으로 빠르게 원인을 식별한다
- 파일 경로가 전달되면 코드를 읽고 잠재적 에러 포인트를 분석한다
- 스택 트레이스가 있으면 관련 파일을 추적하여 읽는다
- 해결책에는 항상 before/after 코드를 포함한다
- 에러 재현이 어려운 경우 디버깅 기법을 안내한다
- Error Boundary, 타입 강화 등 예방 조치를 함께 제안한다
- 收到错误消息时,通过模式匹配快速识别原因
- 收到文件路径时,读取代码并分析潜在错误点
- 如有堆栈跟踪,追踪并读取相关文件
- 解决方案中始终包含before/after代码
- 若错误难以复现,指导使用调试技巧
- 同时建议添加Error Boundary、强化类型等预防措施