Files
sam-react-prod/sam-docs/frontend/v1/14-error-handling.md

4.7 KiB

14. 에러 핸들링

대상: 프론트엔드 개발자 최종 업데이트: 2026-03-20


목차

번호 항목
14.1 API 에러 핸들러
14.2 에러 클래스
14.3 컴포넌트에서 에러 처리
14.4 Next.js 에러 파일
14.5 에러 표시 패턴

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가지 존재하므로 폴백 처리:

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

class ApiError extends Error {
  constructor(
    public status: number,
    public message: string,
    public errors?: Record<string, string[]>  // 필드별 에러 메시지
  ) {}
}

DuplicateCodeError

class DuplicateCodeError extends ApiError {
  constructor(
    public message: string,
    public duplicateId: number,
    public duplicateCode: string
  ) {}
}

getErrorMessage 유틸리티

import { getErrorMessage } from '@/lib/api/error-handler';

// 에러 객체에서 사용자 친화적 메시지 추출
const message = getErrorMessage(error);
// ApiError: "[422] 입력값을 확인해주세요."
// Error: error.message
// unknown: "알 수 없는 오류가 발생했습니다"

14.3 컴포넌트에서 에러 처리

기본 패턴 (Server Action 호출)

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 에러 상세 표시

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에서 구분 필요:

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 필수 구조

'use client';

export default function Error({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>오류가 발생했습니다</h2>
      <p>{error.message}</p>
      <button onClick={reset}>다시 시도</button>
    </div>
  );
}

global-error.tsx 필수 구조

'use client';

// 반드시 <html>, <body> 포함
export default function GlobalError({ error, reset }: { error: Error; reset: () => void }) {
  return (
    <html>
      <body>
        <h2>오류가 발생했습니다</h2>
        <button onClick={reset}>다시 시도</button>
      </body>
    </html>
  );
}

14.5 에러 표시 패턴

상황 방법
API 호출 실패 toast.error(getErrorMessage(error))
폼 검증 실패 Zod + react-hook-form 자동 표시
필드별 서버 에러 error.errors 파싱 후 toast
치명적 에러 error.tsx 에러 바운더리
네트워크 에러 toast + 재시도 안내

금지: alert(), confirm(), prompt() 사용 금지 -> toast (sonner) 또는 Dialog 사용