자바스크립트 오류 처리와 안전한 복구 전략

자바스크립트 오류 처리와 안전한 복구 전략 관련 이미지
Photo by Pexels via Pixabay

서비스에서 오류는 피해야 할 예외적인 사건만이 아닙니다. 사용자가 잘못된 값을 입력하거나 네트워크가 끊기고 외부 API가 제한 시간을 넘는 상황은 일상적으로 발생합니다. 모든 오류를 하나의 catch에서 처리하면 사용자는 무엇을 해야 할지 모르고 개발자는 원인을 찾기 어렵습니다. 자바스크립트 실무에서는 예상 가능한 업무 실패와 프로그래밍 결함을 구분하고 각 계층이 처리할 책임을 명확히 하는 것이 중요합니다.

Error 객체를 문자열 대신 사용하기

throw '실패'처럼 문자열을 던지면 오류 종류와 스택 정보를 일관되게 다루기 어렵습니다. Error 또는 이를 상속한 사용자 정의 오류를 사용합니다. ValidationError, AuthenticationError처럼 의미 있는 종류와 안정적인 오류 코드를 정의하면 호출자가 메시지 문자열을 비교하지 않고 대응할 수 있습니다. 사용자 메시지와 개발자용 상세 정보는 분리합니다.

catch에서는 알 수 없는 값으로 시작하기

자바스크립트에서는 무엇이든 throw할 수 있어 catch의 값이 반드시 Error라고 보장되지 않습니다. TypeScript에서는 unknown으로 보고 error instanceof Error로 좁힌 뒤 name과 message를 사용합니다. 외부 라이브러리의 오류 구조도 버전마다 달라질 수 있으므로 필요한 필드를 안전하게 확인합니다. 전체 응답과 인증 헤더를 로그에 그대로 남기지 않습니다.

복구할 수 있는 계층에서만 잡기

저수준 API 함수가 오류를 기록하고 빈 배열을 반환하면 상위 화면은 실제로 데이터가 없는 것과 서버 실패를 구분하지 못합니다. 해당 계층이 재시도하거나 대체값을 제공할 근거가 없다면 의미 있는 오류로 변환해 다시 던집니다. 같은 오류를 모든 계층에서 기록하면 로그가 중복되므로 요청 경계나 최종 작업 처리기처럼 한 위치에서 문맥과 함께 기록하는 규칙을 정합니다.

실무 원칙: catch는 오류를 없애는 장소가 아니라 복구, 변환, 정리 중 하나를 책임 있게 수행하는 장소입니다.

finally에서 자원과 화면 상태 정리하기

로딩 표시, 잠금 해제, 임시 자원 정리는 성공과 실패 모두에서 필요하므로 finally를 사용합니다. 단, finally에서 return하면 앞선 반환값이나 오류를 덮을 수 있으므로 피해야 합니다. fetch 요청은 AbortController로 취소하고 이벤트 리스너와 타이머도 화면 생명주기에 맞춰 제거합니다.

오류 처리 체크포인트

  • 업무 오류와 시스템 오류에 안정적인 코드를 부여합니다.
  • 원래 원인을 보존해 새 Error의 cause에 연결합니다.
  • 민감한 입력과 토큰을 로그에서 마스킹합니다.
  • 사용자에게 다음 행동이 가능한 메시지를 제공합니다.
  • 복구할 수 없는 오류는 조용히 무시하지 않습니다.

fetch의 성공 여부를 명시적으로 확인하기

fetch는 404나 500 응답만으로 Promise를 거부하지 않습니다. 네트워크 자체 실패에는 거부되지만 HTTP 상태는 response.ok를 확인해야 합니다. 오류 본문이 항상 JSON이라고 가정하지 말고 Content-Type과 파싱 실패를 처리합니다. 서버의 내부 오류 메시지를 그대로 화면에 보여 주지 않고 상태 코드와 애플리케이션 오류 코드를 기준으로 안전한 문구를 선택합니다.

타임아웃과 취소는 실패와 구분하기

사용자가 검색어를 바꿔 이전 요청을 취소한 상황은 경고할 시스템 장애가 아닙니다. AbortError를 별도로 구분해 불필요한 오류 알림과 로그를 만들지 않습니다. 제한 시간 초과는 AbortController와 타이머로 구현할 수 있으며 타이머는 finally에서 정리합니다. 취소 신호를 하위 함수까지 전달해야 실제 네트워크와 후속 작업이 멈춥니다.

재시도 가능한 오류만 선택하기

일시적인 502, 503, 네트워크 단절은 제한적으로 재시도할 수 있지만 400 입력 오류와 401 인증 오류를 반복해도 해결되지 않습니다. 지수 백오프와 무작위 지연, 최대 횟수와 총 제한 시간을 둡니다. POST 작업을 재시도할 때는 멱등성 키가 없으면 중복 주문이나 결제가 생길 수 있습니다. 서버가 Retry-After를 제공하면 가능한 범위에서 따릅니다.

Promise 오류를 놓치지 않기

async 함수는 Promise를 반환하므로 호출자가 await하거나 catch를 연결해야 합니다. 이벤트 핸들러에서 의도적으로 기다리지 않는 작업도 실패 처리를 명시합니다. Promise 체인의 catch에서 값을 반환하면 이후 체인은 성공 상태로 이어지므로 정말 복구된 것인지 확인해야 합니다. Promise.all은 하나가 실패하면 전체가 거부되지만 다른 작업이 자동으로 취소되지는 않습니다.

화면 오류 경계와 대체 UI

프레임워크 컴포넌트 렌더링 오류는 화면 일부를 대체 UI로 바꿔 전체 앱이 빈 화면이 되는 것을 막을 수 있습니다. Vue에서는 errorCaptured와 앱 수준 오류 처리기를 목적에 맞게 사용합니다. 오류 경계가 데이터 손상 상태에서 작업을 계속하게 해서는 안 되며, 새로고침·이전 화면·문의 방법 같은 안전한 선택을 제공합니다.

관측성과 개인정보

로그에는 요청 ID, 오류 코드, 배포 버전, 발생 위치, 처리 시간을 포함하면 원인 추적에 도움이 됩니다. 사용자 입력 전체와 응답 본문은 수집하지 말고 필요한 필드만 마스킹합니다. 같은 오류가 폭발적으로 발생할 때 로그 시스템까지 과부하되지 않도록 샘플링과 집계를 적용합니다. 오류율뿐 아니라 영향을 받은 사용자 수와 업무 실패율을 함께 봅니다.

테스트 순서

  1. 유효성 실패와 권한 실패의 메시지를 확인합니다.
  2. HTTP 4xx, 5xx와 비 JSON 오류 본문을 시험합니다.
  3. 네트워크 단절, 지연, 사용자 취소를 구분합니다.
  4. 재시도 중 중복 작업이 없는지 검증합니다.
  5. finally에서 로딩과 타이머가 정리되는지 봅니다.
  6. 처리되지 않은 Promise 거부를 테스트 실패로 감지합니다.
  7. 로그에 비밀값과 개인정보가 없는지 검사합니다.

오류 처리는 try/catch 문법을 추가하는 작업이 아니라 실패했을 때 시스템과 사용자가 어떤 상태에 남는지를 설계하는 일입니다. 오류 종류와 책임 계층, 재시도와 취소, 관측 규칙을 함께 정하면 예외 상황에서도 서비스가 예측 가능하게 동작하고 문제를 더 빠르게 해결할 수 있습니다.

한 줄 요약: 자바스크립트 오류 처리는 실패를 숨기지 말고 종류별로 복구·전달·기록 책임을 나눠 안전한 다음 행동을 제공해야 합니다.