---
type: knowledge
domain: frontend
status: active
last-reviewed: 2026-07-27
---

# 프론트엔드 에러 처리와 관측

> 한 줄 정의
> 프론트 에러는 서버 로그에 안 남는다 — **수집하지 않으면 존재하지 않는 결함**이 된다. 3상태·실패 UX 원칙은 [[프론트엔드 원리]], 여기는 에러를 어디서 잡고, 뭐라고 보여주고, 어떻게 남기는지의 실전이다.

## 무성 실패 금지 — 제1규율

콘솔에만 남는 에러는 사용자에게도 개발팀에게도 보이지 않는 **미발견 결함**이다. 모든 에러는 두 갈래로 반드시 흐른다:

1. **사용자에게** — 상황 인지와 다음 행동 (아래 문구 규율)
2. **수집 채널로** — 개발팀이 빈도·재현 조건을 볼 수 있게 (아래 로깅)

`catch {}` 빈 블록, 실패한 요청의 미처리 promise, "에러 나면 그냥 이전 화면 유지"는 전부 무성 실패다.

예외는 예상된 흐름뿐이다 — 사용자 취소(`AbortError`)·폼 검증 실패는 로컬 UX만 만들고 원격 수집에서 뺀다. 수집 채널은 결함용이다.

## 에러 바운더리 — 배치 단위가 곧 폭발 반경

| 배치 위치 | 역할 |
|----------|------|
| 앱 루트 | 최후 안전망 — 백지 화면 방지, "문제가 발생했습니다"+새로고침 |
| 라우트(페이지) 단위 | 기본값 — 한 페이지의 렌더 오류가 앱 전체를 죽이지 않게 |
| 독립 위젯 단위 (대시보드 카드·사이드 패널) | 위젯 하나의 실패가 나머지 화면을 살려두게 |

- 루트 하나만 두는 것은 배치가 아니라 방치다 — 어떤 컴포넌트가 죽어도 전체 백지가 된다.
- 바운더리는 **렌더 오류**만 잡는다 — 이벤트 핸들러·비동기 오류는 아래 전역 핸들러와 데이터 레이어의 몫. "바운더리 있으니 됐다"가 최다 오해다.
- 바운더리 폴백에도 재시도 버튼을 둔다 — 일시 오류를 새로고침 없이 복구할 수 있게.

## 전역 핸들러 — 마지막 그물

- `window.onerror`(동기 예외)와 `unhandledrejection`(미처리 promise 거부) 둘 다 등록 — 후자 누락이 단골이며, 비동기 중심 코드에서는 에러 대부분이 이 그물로 온다.
- 전역 핸들러의 역할은 **수집·보고**다. 여기서 사용자 UX를 만들려 하면 늦다 — UX는 바운더리와 각 데이터 레이어에서.
- API 에러의 공통 처리(401 갱신, 5xx 안내)는 데이터 레이어 한 곳에 — 화면마다 catch를 복붙하면 처리 정책이 화면 수만큼 갈라진다 ([[프론트엔드 원리]] 인증 흐름).

## 사용자에게 보이는 실패 문구 규율

| 금지 | 대신 |
|------|------|
| 스택·서버 raw 메시지·내부 코드명 노출 | 사람 말로 상황 요약 + traceId 등 문의용 식별자만 |
| "오류가 발생했습니다" 단독 | 무엇이 실패했고(저장 안 됨) 다음 행동이 뭔지(재시도·문의) |
| 침묵 (실패했는데 화면 변화 없음) | 실패는 반드시 화면에 — 무성 실패 금지 |

- 재시도 가능한 실패(네트워크·5xx)와 불가능한 실패(권한·검증)를 문구와 행동 버튼에서 구분한다. 폼 검증 실패의 표시는 [[폼·입력 검증 실전]], 문구 작법은 [[UX 라이팅 원리]].
- 서버가 traceId를 응답에 주면 화면에 함께 노출 — "문의 시 이 코드를 알려주세요"가 현업 문의→로그 추적을 한 번에 잇는다. traceId·에러 코드의 서버 측 규약은 [[에러 처리와 로깅 실전]].

## 프론트 로깅·소스맵

- 수집 항목: 에러 메시지·스택, 발생 URL(쿼리스트링 제거 — 검색어·토큰이 URL에 실려 온다)·행동 맥락(마지막 액션), 앱 버전, 브라우저. **개인정보·입력값 원문은 금지** — 폼 값·토큰이 에러 페이로드에 딸려 가는 사고가 단골이다 ([[개인정보와 컴플라이언스]]).
- 프로덕션 번들은 압축돼 스택이 무의미하다 — **소스맵을 빌드마다 수집 도구에 업로드**하고(공개 배포 금지), 에러에 앱 버전을 태워 어느 빌드의 스택인지 대응시킨다. 소스맵 없는 프론트 수집은 "에러가 났다"만 아는 반쪽이다.
- 같은 에러의 폭풍(렌더 루프 등)에 대비해 클라이언트 측 중복 제거·전송 상한을 둔다.

## 폐쇄망(발주사 환경) — 외부 APM 불가 시 대안

- Sentry 등 외부 SaaS 전송은 폐쇄망에서 원천 차단이다. 대안은 **자가 수집 엔드포인트**: 프론트가 에러를 백엔드의 `/client-logs` 류 API로 POST → 백엔드 로그 파이프라인에 합류시켜 서버 로그와 같은 곳에서 조회. 이 엔드포인트는 사실상 공개 입력이다 — 본문 크기·배치 건수 상한, 스키마 검증, rate limit을 서버가 강제한다.
- 이때도 규율은 동일하다 — 구조화(JSON)·버전 태깅·개인정보 금지·전송 상한. 수집 서버가 죽어도 앱은 멀쩡해야 하므로 전송 실패는 조용히 버린다(이것만은 예외적으로 무성이어도 된다).
- 에러 로그도 발주사 데이터다 — 수집·보관 위치는 발주사 인프라 안이어야 하며, 외부 반출은 계약 위반이다.

## 안티패턴

- try/catch로 감싸고 콘솔 출력 후 정상 흐름 계속 — 데이터가 반쯤 망가진 채 진행되는 최악의 형태.
- 모든 에러를 토스트 하나로 — 페이지가 죽었는데 토스트만 뜨고 백지가 남는다. 폭발 반경에 맞는 계층(바운더리·필드·토스트)을 쓴다.
- 출시 후 관측 도입 — 수집이 없던 기간의 결함은 영원히 "고객이 이상하대요"로만 남는다. 검수 전에 수집이 돌고 있어야 검수 중 결함이 재현 가능해진다.

## 관련 문서

- [[프론트엔드 원리]] — 3상태 원칙·AI 실패 UX·인증 흐름
- [[폼·입력 검증 실전]] — 검증 실패의 필드 표시
- [[UX 라이팅 원리]] — 실패 문구 작법
- [[개인정보와 컴플라이언스]] — 로그의 PII 금지
- [[에러 처리와 로깅 실전]] — 서버 측 에러 분류·응답 규약·traceId
- [[장애 대응 실전]] — 수집된 에러가 장애가 됐을 때
- [[디버깅 원리]] — 스택에서 원인으로
- [[00_프론트엔드 허브]]
