Skip to Content
영화 리뷰 미션알림 한 줄로 뭉뚱그려지는 에러

알림 한 줄로 뭉뚱그려지는 에러

카테고리: error-handling · 이 패턴은 8기 PR 26건 중 6건에서 관찰됩니다.

Result/커스텀 에러를 도입했지만 알림 시점에서 다시 평탄화한다.

ApiError, ResponseError, Result 객체로 에러 타입을 분리해 두고도, 컨트롤러나 View의 마지막 표시 단계에서 동일한 alert이나 문자열로 다시 통일해 표시하는 패턴입니다. 분리한 에러 타입의 정보가 마지막 한 단계에서 모두 손실됩니다.

문제 코드

다음은 실제 8기 크루 PR에서 추출한 코드입니다. 작성자는 익명 처리하고 원본 PR 링크만 남깁니다.

사례 1

} catch (error) { if (error instanceof ResponseError) { if (error.type === "HTTP") { movieListView.errorRender(ERROR_MESSAGE.HTTP); return; } if (error.type === "NETWORK") { movieListView.errorRender(ERROR_MESSAGE.NETWORK); return; } movieListView.errorRender(ERROR_MESSAGE.DEFAULT); } }

ResponseError가 아닌 경우 catch 블록이 사실상 비어 있어 예상치 못한 오류가 조용히 사라집니다. 원본: PR #272  · src/controller/popularController.ts

사례 2

} catch (e) { const errorMessage = e instanceof Error ? e.message : "알 수 없는 오류가 발생했습니다."; return { success: false, error: errorMessage }; }

Result 패턴을 도입했지만 error 필드를 string 한 줄로 좁혀서 status code와 타입을 잃어버립니다. 원본: PR #284  · src/api/fetchFromApi.ts

사례 3

private handleSearchError(error: unknown) { console.error("검색 중 에러:", error); alert("검색 중 문제가 발생했어요"); this.moreButton.hide(); }

console.error에는 원본을 남기지만 사용자 경로는 단일 alert로 평탄화되어 분리된 에러 타입이 의미를 잃습니다. 원본: PR #282  · src/components/SearchForm.ts

스스로 진단해보기

해설을 펼치기 전에 다음 질문에 답한다.

  1. 에러 타입을 분리한 이유가 마지막 표시 단계에서 살아 있는지 확인한다.
  2. 사용자 메시지가 동일하더라도 로깅이나 재시도 정책이 달라야 하는 케이스를 하나 적는다.
  3. 이 catch가 처리하지 못하는 에러가 어디로 흘러가는지 추적해 본다.

해설

해설 보기

타입을 나누는 일과 그 타입에 따라 동작을 달리하는 일은 별개입니다. ResponseError, ApiError, Result처럼 구조화된 에러 타입 을 만들어 두었더라도, 마지막 표시 계층에서 모두 동일한 alert 한 줄로 변환되면 상위에서 분기한 의미는 사라집니다.

이 패턴은 두 가지 손해를 만듭니다. 첫째, 모니터링이 무뎌집니다. 네트워크 오류와 4xx 응답이 같은 메시지로 집계되면 어디를 고쳐야 할지 우선순위를 잡을 수 없습니다. 둘째, 재시도와 인증 갱신 같은 정책 분기가 불가능해집니다. 401은 토큰 재발급, 5xx는 재시도, 4xx는 사용자 입력 수정 안내처럼 서로 다른 처리가 필요한데 평탄화된 catch에서는 이 분기를 걸 위치 자체가 사라집니다.

또 한 가지 함정은 instanceof 분기 뒤에 else가 없다는 점입니다. 예상치 못한 에러는 catch 블록 밖으로 다시 던져 전역 에러 핸들러 나 상위 경계로 흘려야 추적할 수 있습니다. 분기에서 빠진 에러를 그냥 삼키면 디버깅이 가장 어려운 종류의 침묵 버그가 됩니다.

개선 방향

Before

} catch (e) { const errorMessage = e instanceof Error ? e.message : "알 수 없는 오류"; return { success: false, error: errorMessage }; }

After

} catch (e) { if (e instanceof ResponseError) { return { success: false, error: e }; } throw e; }

핵심 변화는 분류된 에러 타입을 마지막 단계까지 보존하고, 분류되지 않은 에러는 다시 던져 상위 경계가 처리하도록 위임했다는 점입니다.

더 알아볼 개념

Last updated on