경험적으로 에러를 하나의 함수, 즉 하나의 ErrorBoundary에서 처리하긴 힘들 것 같다는 느낌이 들었습니다.
따라서 대략적으로 구상했던 핸들링 흐름은 다음과 같습니다.
에러 발생
특정 ErrorBoundary에서 catch
특정 에러인지 검사
조건에 부합하지 않는다면 throw
특정 ErrorBoundary에서 catch
특정 에러인지 검사
조건에 부합하지 않는다면 throw
… 반복
조건에 부합하는 경우 적절하게 대처
다음은 위 흐름을 적용하기에 앞서 필요할 것이라고 예상되는 것들입니다.
- 커스텀 에러
- ErrorBoundary
이번 글에서는 구상한 핸들링 흐름을 구현하기 위해 필요한 자재들을 마련하는 과정과 고민을 얘기하고자 합니다.
커스텀 에러
수복이 가능 여부에 따라 에러를 분류하고 더 많은 정보를 담기 위해 서비스 전반에서 사용할 커스텀 에러를 구상했습니다.
- 런타임에 직접 발생시킨 에러
- 통신이 성공한 API 에러
그리고 이 외의 모든 에러는 예측이 힘든 에러라고 판단하여
- 예상할 수 없는 에러
이렇게 3가지로 분류했습니다.
코드로 살펴보면 다음과 같습니다
ZipgoError
커스텀 에러들의 베이스가 되는 에러입니다.
기본적으로 자바스크립트 내장 객체 Error
의 인터페이스를 따르는 것이 사용하는 측면에서도 편리할 것이라 생각했습니다.
에러 코드를 통해 에러의 정보를 파악하고 name
, message
와 같은 정보들을 저장합니다.
어떤 데이터가 오류 위험이 있는지 명확히 예측 가능한 경우 value
속성을 통해 원인이 되는 데이터도 저장합니다.
ignore
속성은 ErrorBoundary와 함께 설명하겠습니다.
RuntimeError
ignore
속성이 true
인 점을 제외하면 ZipgoError
와 같습니다.
UnexpectedError
에러 코드가 UNEXPECTED_ERROR
인 에러입니다.
ignore
속성이 true
인 점, 에러 코드가 고정된 점을 제외하면 ZipgoError
와 같습니다.
예상할 수 없는 에러이기에 대부분의 상황에선 런타임에 변환된 결과일 것으로 예상되었습니다.
다만, switch... case
와 같은 특정 패턴 매칭 상황에선 개발자가 직접 발생시킬 수도 있을 것 같아 디버깅에 편리하지 않을까? 하고 value
속성을 남겨두었습니다 😂
APIError
APIError
는 통신이 성공한 경우 사용되는 에러입니다.
서버에서 제공하는 에러 코드와 상태 코드를 저장합니다.
canManage
메서드는 발생한 APIError
가 핸들링 가능한 에러인지 파악합니다.
저희 서비스는 모든 api call에 axios
를 사용하고 axios
응답 객체는 양식이 정해져 있습니다.
그에 따라 제가 결정한 핸들링 가능 기준은 다음과 같습니다.
응답 객체에 다음 속성이 있는가?
- config
- request
- response
위 속성들은 다양한 이유로 응답 객체에 존재하지 않을 수 있습니다. 그리고 위 속성들이 없다면 에러에 대한 정보를 알 수 없다는 공통점이 있습니다.
따라서 canManage
함수를 통과하지 못한 에러는 UnexpectedError
로 분류하였습니다.
ErrorBoundary
다양한 에러를 분류하여 처리하고, 이 과정을 UI와 관심사를 분리하기 위해 ErrorBoundary
를 사용하였습니다.
기본적인 인터페이스는 ErrorBoundary와 크게 다르지 않습니다.
다루고자 하는 에러의 형태
에러가 발생하여 최초로 ErrorBoundary에 도착한다면 이 에러는 이제 제가 구현한 흐름을 타고 코드 전반으로 올라가게 될 것입니다.
흘러가는 에러를 건져올렸을 때 어떤 형태가 확장성이 좋을지, DX적으로 편리한 인터페이스일지 고민했습니다.
원본 에러를 훼손한다면 혹시 모를 활용 가능성을 저버리는 것이고..
원본 에러를 throw
한다면 사용하는 쪽에서 매번 에러를 변환하는 과정을 거쳐야 할 것입니다.
고민 끝에 당장 원본 에러를 활용할 부분이 예상되지 않고 추후 원본 에러를 사용하도록 변경하는 것이 큰 리소스를 필요로 할 것 같지 않아 원본 에러를 커스텀 에러로 변환하여 throw
하도록 결정했습니다.
이제 하나의 ErrorBoundary를 통과한 에러는 모두 커스텀 에러로 변환될 것입니다.
fallback render props
위와 같이 사용하니 fallback이 에러의 정보를 알 수 없어 ErrorBoundary마다 일일이 적절한 fallback UI를 적용해야만 하는 문제가 발생했습니다.
이를 해결하고자 fallback의 형태를 render props도 가능하도록 변경하였습니다.
이제 사용처에서 원하는 형태로 fallback을 적용할 수 있습니다.
fallback의 타입에 따라 렌더링하도록 render
메서드도 수정해 주었습니다.
이렇게 변경하니 좀 더 유연한 fallback UI를 사용할 수 있었습니다.
onError
에러가 발생했을 때 실행되며 함수의 인자에 변환된 에러가 전달됩니다.
구상한 흐름에 따르면 ErrorBoundary에서 catch
한 에러는 현재 ErrorBoundary와 같은 관심사를 갖고 있는지 검증된 후 throw
여부가 결정됩니다.
이 과정을 구현할 수 있는 두 가지 방법이 떠올랐습니다.
- BaseErrorBoundary를 확장한 특정 ErrorBoundary(클래스 형 컴포넌트)
- BaseErrorBoundary를 Wrapping한 특정 ErrorBoundary(함수 형 컴포넌트)
1번의 방식으로 구현하면 getDerivedStateFromError
또는 componentDidCatch
를 오버라이딩하여 해당 메서드 내부에서 에러 검증을 거치면 될 것입니다.
실제로 구현해보니 정상적으로 동작하는 것을 확인하였습니다.
다만 컴포넌트 간 상속은 React에서 지양하는 방식이며 개발자들 사이에서도 컴포넌트 상속에 대해 많은 논란이 있었습니다.
위와 같이 간단한 오버라이딩은 사용에 문제가 없을 것이라 생각되지만, 앞으로의 확장성을 고려한다면 익숙하지 않은 초행길을 위험 부담을 안은 채 가고 싶지 않았습니다.
따라서 제가 선택한 방법은 2번입니다.
다만 2번을 선택하니 앞서 말한 에러 검증 로직을 실행할 곳이 마땅치 않았습니다.
현재 코드로는 ErrorBoundary의 생명주기에 직접 접근하는 방법이 없기 때문입니다.
그렇게 탄생한 props
가 onError
입니다.
간단하게 componentDidCatch
에서 onError
를 실행시켜 주었습니다.
이제 ErrorBoundary가 에러가 발생하였을 때 사용하는 쪽에서 원하는 로직을 주입할 수 있게 되었습니다.
reset
불가피하게 에러가 발생하여 에러 fallback을 보여주게 되었다면 사용자에게 다음 행동 수단을 제시해 주어야 합니다.
일반적으로 많이 제공되는 행동 수단은 ‘홈으로 돌아가기’, 그리고 ‘다시 시도하기’입니다.
하지만 ErrorBoundary에 에러가 catch
된 순간 hasError
가 true
로 변하기 때문에 이것을 초기화 시켜주지 않는 이상 항상 fallback UI를 렌더링 하게 됩니다.
이 문제를 해결하고자 reset
메서드를 구현했습니다.
간단하게 상태를 초깃값으로 변경해 주는 메서드입니다.
다만 이렇게 끝낸다면 reset
메서드를 ErrorBoundary 외부에서 활용할 수 없습니다.
따라서 예상되는 주 사용처인 fallback에서 사용할 수 있도록 render props 인자로 함께 넣어주었습니다.
치명적인 에러
RuntimeError
, UnexpectedError
와 같이 수복이 어려운 에러나 혹은 APIError
라도 페이지에서 핵심적인 API인 경우 에러 페이지를 보여주어야 할 것입니다.
다만 생각해 보니 ErrorBoundary가 겹겹이 쌓인 컴포넌트 트리 깊숙한 곳에서 발생한 에러는 에러 페이지를 보여주기 힘들다는 문제가 예상되었습니다.
다음과 같은 경우입니다.
이 같은 문제를 해결하기 위해선 에러 페이지를 렌더링 하기 원하는 경우 에러를 최상단까지 throw
시켜주어야 할 필요가 있었습니다.
따라서 발생한 에러가 최상단까지 도달해야 하는 에러라면 중간의 어떤 ErrorBoundary에도 catch
되지 않도록 ignore
속성을 통해 검사하였습니다.
이것이 RuntimeError
와 UnexpectedError
에 ignore
속성이 존재하는 이유입니다.
에러를 무시할 수 있도록 componentDidCatch
를 변경해 주었습니다.
그리고 최상단에서 ErrorBoundary로 감싸줍니다.
이제 ignore
속성을 설정하여 최상단에서 에러 페이지를 렌더링 할 수 있을 것입니다.
맺으며
본격적으로 에러 핸들링을 적용하기에 앞서 ErrorBoundary를 필요한 만큼 보강해 보았습니다.
실제로 적용하는 과정에서는 추가적으로 필요한 기능들이 있어 변경 사항이 많이 발생했습니다.
다음 글에서는 이 자재들을 활용하는 과정에서 어떤 것이 부족했는지, 어떻게 활용했는지 얘기하겠습니다.
감사합니다.
참고