# 14. 에러 핸들링 > 대상: 프론트엔드 개발자 > 최종 업데이트: 2026-03-20 --- ## 목차 | 번호 | 항목 | |------|------| | 14.1 | [API 에러 핸들러](#141-api-에러-핸들러) | | 14.2 | [에러 클래스](#142-에러-클래스) | | 14.3 | [컴포넌트에서 에러 처리](#143-컴포넌트에서-에러-처리) | | 14.4 | [Next.js 에러 파일](#144-nextjs-에러-파일) | | 14.5 | [에러 표시 패턴](#145-에러-표시-패턴) | --- ## 14.1 API 에러 핸들러 `src/lib/api/error-handler.ts`에서 HTTP 상태별 처리: | 상태코드 | 처리 | 동작 | |---------|------|------| | 401 | 인증 만료 | `/login`으로 리다이렉트 | | 403 | 권한 없음 | ApiError throw | | 400 + `duplicate_id` | 품목코드 중복 | DuplicateCodeError throw | | 422 | Validation 에러 | 상세 필드 에러 포함 ApiError throw | | 기타 | 일반 에러 | ApiError throw | ### 422 Validation 에러 폴백 API 응답 포맷이 2가지 존재하므로 폴백 처리: ```typescript // data.errors (Laravel 기본) || data.error.details (커스텀 포맷) const validationErrors = data.errors || data.error?.details; ``` 이 폴백은 다음 3곳에 동일하게 적용: - `error-handler.ts` (Server Action 경로) - `client.ts` (클라이언트 API 경로) - `index.ts` (직접 fetch 경로) --- ## 14.2 에러 클래스 ### ApiError ```typescript class ApiError extends Error { constructor( public status: number, public message: string, public errors?: Record // 필드별 에러 메시지 ) {} } ``` ### DuplicateCodeError ```typescript class DuplicateCodeError extends ApiError { constructor( public message: string, public duplicateId: number, public duplicateCode: string ) {} } ``` ### getErrorMessage 유틸리티 ```typescript import { getErrorMessage } from '@/lib/api/error-handler'; // 에러 객체에서 사용자 친화적 메시지 추출 const message = getErrorMessage(error); // ApiError: "[422] 입력값을 확인해주세요." // Error: error.message // unknown: "알 수 없는 오류가 발생했습니다" ``` --- ## 14.3 컴포넌트에서 에러 처리 ### 기본 패턴 (Server Action 호출) ```typescript try { const result = await saveItem(formData); if (!result.success) { toast.error(result.error || '저장 실패'); return; } toast.success('저장 완료'); } catch (error) { if (isNextRedirectError(error)) throw error; // redirect는 재throw toast.error(getErrorMessage(error)); } ``` ### Validation 에러 상세 표시 ```typescript if (error instanceof ApiError && error.errors) { const firstKey = Object.keys(error.errors)[0]; const firstError = error.errors[firstKey]; toast.error(`${error.message}\n${firstKey}: ${firstError[0]}`); } ``` ### redirect 에러 구분 Next.js의 `redirect()`는 내부적으로 에러를 throw하므로 catch에서 구분 필요: ```typescript import { isNextRedirectError } from '@/lib/utils/redirect-error'; catch (error) { if (isNextRedirectError(error)) throw error; // 반드시 재throw // 실제 에러만 처리 } ``` --- ## 14.4 Next.js 에러 파일 | 파일 | 용도 | 'use client' | 위치 우선순위 | |------|------|-------------|-------------| | `error.tsx` | 런타임 에러 경계 | 필수 | 특정 라우트 > 그룹 > locale > root | | `not-found.tsx` | 404 페이지 | 불필요 | 특정 라우트 > locale > root | | `global-error.tsx` | 루트 레이아웃 에러 | 필수 | root만 | | `loading.tsx` | 로딩 상태 (Suspense) | 불필요 | 특정 라우트 > 그룹 | ### error.tsx 필수 구조 ```typescript 'use client'; export default function Error({ error, reset, }: { error: Error & { digest?: string }; reset: () => void; }) { return (

오류가 발생했습니다

{error.message}

); } ``` ### global-error.tsx 필수 구조 ```typescript 'use client'; // 반드시 , 포함 export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) { return (

오류가 발생했습니다

); } ``` --- ## 14.5 에러 표시 패턴 | 상황 | 방법 | |------|------| | API 호출 실패 | `toast.error(getErrorMessage(error))` | | 폼 검증 실패 | Zod + react-hook-form 자동 표시 | | 필드별 서버 에러 | `error.errors` 파싱 후 toast | | 치명적 에러 | `error.tsx` 에러 바운더리 | | 네트워크 에러 | toast + 재시도 안내 | **금지**: `alert()`, `confirm()`, `prompt()` 사용 금지 -> `toast` (sonner) 또는 `Dialog` 사용