Skip to content

Mutation 에러 안전망 도입 (silent failure 근절) #299

Description

@iOdiO89

Epic: API Status Code 전수 대응 #298
우선순위: P0 (최우선 · 선행)

배경

에러 무 처리의 근본 원인은 개별 훅이 아니라 전역 설정이다.

  • utils/queryClient.tsqueries defaultOptions만 있고 mutations 전역 onError가 없다. MutationCache도 미사용
  • → onError가 비어있거나(// TODO) 없는 mutation은 실패해도 조용히 삼킨다(silent failure). 사용자는 성공한 줄 안다

즉 위시/토너먼트/알림 이슈의 "무처리" 상당수는 개별 수정 이전에 전역 안전망 하나로 대부분 해결된다.

설계 원칙

📖 전체 규약 파일 생성: error-handling-policy.md

  • 4xx = 개별 처리 / 5xx·네트워크 = 전역 처리 (핵심 모델)
  • 5xx는 전역이 단독 권위 — 개별 onError는 5xx를 건드리지 않음 (토스트 중복 방지)
  • 4xx는 개별 우선, 개별 onError 없으면 전역 generic fallback
  • 명시적 처리 = 도달 가능하고 사용자 행동이 바뀌는 것만 (400 검증, 409 만료 등)
  • "정상 UI에선 도달 불가"한 케이스(예: 남의 위시 403)는 개별 분기 X → 안전망이 덮음

대응 방안

  • QueryClientMutationCache 전역 onError 추가 (utils/queryClient.ts)
    • 개별 mutation에 onError가 있으면 스킵 (맞춤 처리 우선)
    • 없으면 generic 토스트 (getApiErrorMessage(error) — 서버 detail 있으면 노출, 없으면 기본 문구)
    • 예상 못한 에러는 Sentry로 캡처 (이미 도입된 Sentry 활용)
  • QueryCache 전역 onError 추가 — Sentry 로깅만 (토스트 X) (query는 배경 refetch 스팸 방지)
  • 공통 에러 메시지 유틸 getApiErrorMessage(error) 정의 (5xx/네트워크/detail 처리 일원화 — #3와 공유)
  • 401(전역 인터셉터)·layout 가드가 이미 처리하는 status와 이중 토스트가 안 뜨는지 확인
// 예시 (utils/queryClient.ts) — 5xx 전역 독점 / 4xx 개별 우선
new QueryClient({
  mutationCache: new MutationCache({
    onError: (error, _vars, _ctx, mutation) => {
      const status = isAxiosError(error) ? error.response?.status : undefined;
      const isServerError = !status || status >= 500; // 5xx + 네트워크

      if (isServerError) {
        toast.error(getApiErrorMessage(error));
        // Sentry.captureException(error);
        return; // 5xx는 전역 독점 → 개별은 5xx 토스트 금지
      }
      if (mutation.options.onError) return; // 4xx: 개별 처리 우선
      toast.error(getApiErrorMessage(error));
    },
  }),
  defaultOptions: { queries: { retry: 1, staleTime: 60_000, refetchOnWindowFocus: false } },
});

⚠️ MutationCache.onError와 개별 onError둘 다 실행됨 → 개별 훅은 반드시 4xx만 분기 (5xx 브랜치 삭제).

⚠️ 개별 onError 정리 규칙 (중요)

전역 4xx fallback은 개별 onError가 "없을 때만" 작동한다 (if (mutation.options.onError) return). 따라서:

  • 비어있는 onError(// TODO만) → "존재"로 취급돼 전역이 양보 → 여전히 silent.삭제할 것
  • 부분 onError(예: 403/404만, 400 빠짐) → 빠진 4xx는 개별도 전역도 안 잡음 → 여전히 silent.도달 가능한 4xx 전부 커버할 것

규칙: onError는 "4xx 전부 책임" 또는 "아예 없음(전역 위임)" 둘 중 하나. 비어있는/부분적인 onError 금지.

  • 전역 net 도입 후, 기존 훅의 비어있는 onError 전부 삭제 (전역 위임 대상)
  • 부분 onError는 도달 가능한 4xx를 채우거나, 맞춤 UX가 없으면 통째로 삭제

완료 조건 (AC)

  • onError 없는 mutation이 실패해도 최소 generic 토스트가 반드시 뜬다 (silent failure 0)
  • 개별 맞춤 onError는 그대로 우선 동작 (이중 토스트 없음)
  • 예상 못한 에러가 Sentry에 기록됨

Metadata

Metadata

Assignees

Labels

refactorExtra attention is needed

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions