에러도 UX다 – React에서의 체계적인 에러 처리 전략
"사용자에게 어떤 에러 경험을 제공하고 있나요?”
1. 들어가며
웹 서비스가 점점 복잡해지고 기능이 늘어날수록, 다양한 이유로 예기치 않은 에러가 발생할 수 있습니다.
서버 API 호출이 실패하거나, 인증 토큰이 만료되거나, 클라이언트 코드에서 예외가 발생할 수도 있죠.
이럴 때 사용자에게 아무런 안내 없이 화면이 멈춰버린다면, 서비스 신뢰도와 사용 경험에 적지 않은 영향을 줄 수 있습니다. 반면, 에러를 예측하고 적절하게 처리하는 구조를 갖추면, 사용자에게 안정적인 인상을 줄 수 있고, 개발자 입장에서도 유지보수와 디버깅이 쉬워집니다.
이 글에서는 리액트 환경(React + Vite)에서 구현한 에러 처리 시스템의 설계 구조와 적용한 과정을 공유하려고 합니다.
2. 에러를 분류하자
먼저, 다양한 에러 상황을 발생 원인과 처리 방식에 따라 분류했습니다.
에러 유형 | 예시 | 처리 방식 |
서버 에러 | API 요청 실패 (500, 404 등) | 사용자 메시지 + UI |
클라이언트 에러 | JS 런타임 에러 | ErrorBoundary 처리 |
인증 에러 | 토큰 만료, 401 등 | 자동 로그아웃 + 로그인 리디렉션 |
커스텀 에러 | 특정 UX 흐름 필요 | 컴포넌트에서 개별 처리 |
네트워크 에러 | 인터넷 끊김 등 | Retry 유도 or 토스트 알림 |
3. 에러 처리의 전체 흐름
에러는 Axios에서 시작해 공통 포맷으로 변환되고, 처리 로직을 거쳐 UI까지 이어집니다.
4. Axios에서 시작하는 에러 표준화
Axios는 다양한 에러 형태를 던집니다
- 서버 응답 에러 (error.response 있음)
- 네트워크 에러 (response 없음)
- 인증 실패 (401 → 토큰 갱신 로직 필요)
이 모든 경우를
createServerError(code, message)
로 정형화된 객체로 감싸기 때문에(표준화하기),
이후 로직이 간단해집니다.interceptor 내부의 에러 처리 흐름
- Axios 응답 에러 → status code 확인
- 백엔드에서 제공한 code 우선 사용
- 없으면 HTTP status → 에러 코드 매핑 테이블로 변환
- 에러 메시지까지 합쳐서 createServerError로 감싸기
📌 createServerError
이 함수는 일반 Error 객체에 code 속성을 추가함으로써,
- 타입 가드 (
isServerError
)를 사용 가능하게 하고,
- 모든 에러를 동일한 구조로 다룰 수 있게 만들어줍니다.
즉,
error.code
,error.message
를 기반으로 안정적인 UI 분기 처리가 가능합니다.
5. 에러를 공통 구조로 감싸기
이 구조를 기반으로 에러 메시지, UI 전략, 로그 수집, 리다이렉트 경로까지 일관되게 처리합니다.
6. 서버 에러 코드와 분류
이 구조는 각 에러가 어떻게 처리되어야 하는지를 명확히 해줍니다.
예를 들어
USER_NOT_FOUND
는 단순 알림, DUPLICATE_NICKNAME
은 페이지 이동이 필요한 커스텀 처리로 분리됩니다.7. 에러 분석 및 분류
- axios 에러인지, 클라이언트 에러인지 구분
- 정의된 에러 코드에 따라 사용자 메시지/처리 전략 결정
8. 처리 실행 함수 (handleError
)
9. react-query와 연동
⚠️ throwOnError: true로 설정해야 ErrorBoundary에 위임됩니다.
10. 에러 UI 처리 전략 요약 (예시)
분류 | 처리 방식 | 예시 메시지 |
ALERT | toast.error() | "사용자를 찾을 수 없습니다." |
AUTH | toast + router.replace('/login') | "세션이 만료되었습니다." |
ERROR_BOUNDARY | <ErrorBoundary> + Fallback UI | "예기치 못한 오류가 발생했어요" |
CUSTOM | useMutation({ onError }) 컴포넌트 처리 | "닉네임이 중복되었습니다." |
11. ErrorBoundary와 통합
react-error-boundary의 resetErrorBoundary()는 에러 상태를 초기화하여 다시 렌더링을 유도합니다.QueryErrorResetBoundary
와 함께 쓰면, react-query 캐시까지 초기화해 API 재시도를 할 수 있습니다. ⇒ 이를 통해 사용자에게 “다시 시도하기” 흐름을 제공할 수 있습니다.
12. 커스텀 에러: 컴포넌트에서 직접 처리
커스텀 에러는 컴포넌트 내부 UX 흐름과 밀접하게 연관되어 있기 때문에 전역 처리보다useMutation().onError
에서 직접 처리하는 것이 명확하고 유연합니다.
isCustomError 예시 코드
13. Sentry 로깅 전략
- ✅ 수집
- 사용자 조작 없이 해결할 수 없는 오류
- 디버깅을 위한 API 실패 (e.g. 서버 500, 네트워크 장애)
- ⛔ 제외
- 사용자가 해결할 수 있는 예상 가능한 오류 (e.g. 로그인 실패)
- UX 상 이미 안내된 알림성 에러 (toast로 처리되는 404 등)
💡 Tip: 모든 에러를 Sentry에 보내면 ‘노이즈’가 많아지고,중요한 이슈를 놓치기 쉬워집니다.
따라서 다음과 같은 기준을 명확히 세워두는 것이 좋습니다.
- 단순 UX 알림(
toast.error
)으로 충분한 오류는 로깅하지 않고 무시
- 예상 외의 예외 상황(서버 오류, 알 수 없는 에러 등)은 반드시 Sentry로 전송
- 필요하다면
handleError
내부에서 조건 분기로 로깅 여부를 판단
이렇게 하면 불필요한 로깅은 줄이고, 중요한 오류만 집중적으로 모니터링할 수 있습니다.
14. 서버 에러 코드 추가 가이드
- 백엔드와 에러 코드 및 의미 정의 협의
- 백엔드에서 에러 코드 적용 후 프론트에 코드 추가
15. 결론
에러는 언제든 발생할 수 있습니다. 하지만 어떻게 설계하고 대응하느냐에 따라, 사용자 경험은 완전히 달라집니다.
사용자가 문제를 만났을 때 보게 되는 UI는 단순한 화면이 아니라, 우리가 제품을 얼마나 책임감 있게 만들고 있는지를 보여주는 부분이라는 생각이 듭니다.
이 글에서 다룬 것처럼,
- 에러를 표준화된 구조로 감싸고
- 카테고리 기반으로 처리 전략을 나누며
- UI와 연결되는 흐름까지 일관되게 설계하면
“에러 처리”는 더 이상 귀찮은 예외가 아니라, 제품의 품질을 지탱하는 강력한 기반이 될 수 있습니다.
“예기치 못한 상황을 '예상 가능한 흐름'으로 바꾸는 것이 제품을 개발하는 사람들에게 주어진 중요한 과제이자 책임이 아닐까요?”
💡 이 글에서 소개한 방식은 하나의 예시일 뿐입니다.
여러분의 서비스에 맞게 조정하며 활용해보시길 바랍니다!