공식에서는 서버와의 통신 과정에서 에러가 발생하였을 때에 어떤 에러인지 안내 메시지를 보여주어 이후 케이스 별로 후 처리를 진행합니다.
지금부터, 공식에서 이러한 과정이 어떻게 진행되고 있는지 어떻게 발전하게 되었는지 자세히 들어보겠습니다 👀
공식의 경우 커스텀 에러코드를 활용하여, 에러를 처리하고 있습니다.
왜 에러 코드를 도입하게 되었을까요?
가장 큰 이유는 "유저에게 적합한 에러 메세지를 보여주기 위해서"입니다.
예를 들어, 서버에서 온 에러메세지를 그대로 로그인을 하는 상황을 가정해봅시다.
공식의 경우 깃허브 OAuth를 통해서 로그인 기능을 구현하였는데요. 만약, 깃허브에서 유저정보를 불러오는 것에 실패할 경우 유저에게 어떤 메세지를 보여주어야 할까요?
유저에게 "깃허브에서 응답받은 토큰이 null입니다"라는 에러 내용을 그대로 보여주는 것이 맞을까요?
유저의 경우 로그인 과정에 문제가 있는지 알 필요가 없습니다. 한 편으로는 알아서도 안됩니다.
올바른 유저가 아닌, 악의적인 사용자가 다른 이의 아이디를 통해서 로그인을 시도 중일 때 "깃허브에서 응답받은 토큰이 null입니다" 라는 등의 자세한 설명은 해킹의 힌트를 제공할 수 있습니다.
또한, 일반 유저의 입장에서는 로그인 시도를 하였을 때에 github 토큰이 null 라는 문구 만으로는 현재 어떤 상황인지 정확하게 추측하기가 힘듭니다. 로그인이 실패했다는 건지, 그러면 다음에는 무엇을 하라는 건지 알기 힘들죠
하지만, 서버에서는 정확한 디버깅을 위해서 전문적인 용어를 사용하여 에러 내용을 표기할 필요가 있었습니다.
그래서 에러케이스 별로 프론트엔드 측에서 유저에게 맞는 에러 메세지를 안내하기 위해 공식팀은 에러코드를 도입하게 되었습니다 😆
공식에서는 에러 바운더리를 도입하기 이전에는 React-Query에서 제공해주고 있는 isError에서 분기문을 통해 케이스 별로 에러를 처리해주었습니다
아래에는 기존의 처리 방법에 대한 간단한 예시입니다.
useEffect(() => {
if(isError){
if(errorCode === '1001' || errorCode === '1002' || .... ){
// 로그인 관련 실패 에러 코드
showSnackBar('로그인에 실패하였습니다 재로그인 해주세요');
if(errorCode === '0000'){
// 서버 에러 코드
showSnackBar('작업에 실패하였습니다 다시 시도해주세요');
}
},[isError]);
하지만 해당 처리 방식에도 단점이 존재하였습니다.
각각의 서버와의 통신마다 나올 수 있는 에러 케이스가 다르기 때문에, 대부분의 경우는 다른 에러코드를 통해서 분기문 처리를 합니다.
하지만 서버에러와 같은 에러상황(서버가 갑자기 동작을 하지 않는다 던지)은 모든 비동기 통신 케이스에서 발생할 수 있습니다.
그래서 모든 비동기 통신에서 반복적으로 if(errorCode === '0000')를 작성해야했습니다.
useEffect문 내에서 isError과 분기문으로 처리하고 있었기 때문에, 커스텀 훅으로 해당 과정을 분리한다 하여도 코드의 길이는 길어질 수 밖에 없었습니다.
그래서 코드의 길이가 늘어나게 되면서 자연스럽게 가독성이 떨어진다는 단점도 존재하였습니다.
앞서 설명된 "중복", "코드 길이 길어짐" 의 단점을 해결하기 위해 공식은 에러바운더리를 도입하기로 하였습니다.
에러바운더리가 무엇일까요? 🧐
리액트 공식 문서에 따르면, 하위 컴포넌트 트리의 어디에서든 자바스크립트 에러를 기록하며 깨진 컴포넌트 트리 대신 fallback UI를 보여주는 React 컴포넌트입니다 클래스 컴포넌트를 사용하여 구현하는 것을 원칙으로합니다.
boundary 뜻 : 경계선
fallback 뜻 : 대비책
클래스 컴포넌트에서의 생명주기 메서드인 static getDerivedStateFromError()와 componentDidCatch() 중 하나를 정의하게 되면 클래스 컴포넌트 자체가 에러 경계가 되게 됩니다.
getDrivedStateFromError() 이 무엇일까요? 🤔
클래스컴포넌트에서 제공하는 생명주기 메서드 중 하나로 하위의 자손 컴포넌트에서 오류가 발생하였을 때에 호출됩니다.
componentDidCatch() 는 무엇일까요?
이 또한 클래스컴포넌트에서 제공하는 생명주기 메서드 중 하나로 자손 컴포넌트에서 오류가 발생하였을 때에 호출되고, 2개의 매개변수를 전달받습니다. error- 발생한 오류, info - 어떤 컴포넌트가 오류를 발생시켰는지에 대한 정보 해당 메서드는 부수효과를 발생시켜도 되게 때문에 부수효과가 있어도 됩니다.
하지만 클래스컴포넌트의 에러바운더리의 경우, 비동기적 코드
에서 발생한 에러는 자동으로 잡아내지 못합니다. 그렇기 때문에 비동기 통신 과정에서 발생하는 에러에 대해서 처리하기 위해 저희 공식 팀은 리액트 쿼리에서 제공하는 isError를 활용하게 되었습니다.
에러가 발생하게 되면 리액트에서 isError가 true를 바뀌고 에러 내용도 error를 통해 제공했기 때문에 이를 통해 에러를 선언적으로 관리하는 방식으로 처리하게 되었습니다.
useEffect(() => {
if(isError) {
throw new CustomError(errorCode, errorMessage);
}
}, [error]);
...
참고로 해당 부분도 컴포넌트 단마다 반복해서 선언되었기 때문에 이러한 중복도 제거하기 위해서 useThrowCustomError에 useEffect문을 넣어 호출하는 방식으로 리팩토링을 진행하였습니다.
그런데 리액트 클래스 컴포넌트에서는 훅을 사용하지 못합니다!
아래는 제가 클래스 컴포넌트에서 훅을 호출하였을 때에 마주한 에러입니다.
왜 클래스컴포넌트에서는 훅을 사용하지 못할까? 🤔
저도 정확한 답을 알고 있는 것은 아니지만, 추측을 해보자면 state와 class, function의 작동 방식 때문에 그렇지 않을까 싶습니다.
클래스의 경우, 인스턴스를 만든 후 해당 인스턴스의 render를 계속해서 재호출하는 방식으로 동작합니다.
이에 반해, 함수의 경우 호출 시 때마다 매번 재생성되고 재호출 되기 때문에 작동방식에 차이점이 있습니다.
훅에서 상태를 선언하고 해당 상태에 변경사항이 있을 때마다 재랜더링이 이루어집니다. 또한 리액트 자체에서 훅이 발생되는 순서를 인덱스테이블 형태로 들고 있기 때문에 클래스 컴포넌트 내에서 훅을 호출하게 되면 이러한 실행순서와 불일치 되어 상태값 관리에 어려움이 있지 않을까 추측됩니다.
그래서 클래스 컴포넌트 내에서 훅을 사용하기 위해서는 HOC로 감싸 HOC내에서 훅들을 선언해 클래스 컴포넌트 내에 props로 넘겨주는 방식으로 처리하여야 합니다.
HOC란 무엇일까? 🤔
HOC는 Higher Order Component의 줄임말로 한국어로 뜻을 변역해보자면 고차 컴포넌트입니다.
고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.
공식에서는 고차함수의 이러한 특징을 활용하여 고차함수 내에서 함수 컴포넌트를 하나 생성하여, 해당 컴포넌트 내에서 커스텀훅들을 호출합니다. 그리고 호출한 커스텀 훅들을 인자로 받아온 클래스컴포넌트에 prop으로 넘겨주어 새로운 컴포넌트 형태로 반환해주고 있습니다.
<ErrorBoundary>
<App />
</ErrorBoundary>
에러는 전파된다는 특징을 활용하여 공식에서 만든 useThrowCustomError를 통해 던진 에러들을 모두 에러바운더리에서 받기 위해 App 전체를 에러바운더리로 감싸게 되었습니다.
현재 공식에서는 에러바운더리를 CommonErrorBoudary, LogicErrorBoundary, FallbackErrorBoundary등 3가지로 나누어 에러처리를 진행하고 있습니다.
CommonErrorBoundary로 남은 두가지의 에러바운더리에서 공통적으로 사용되고 있는 로직을 정의하여, FallbackErrorBoundary와
LogicErrorBoundary에서 상속받도록 구현하였습니다.
CommonErrorBoundary에서는
하위의 컴포넌트에서 오류가 발생했을 때 호출되는 getDerivedStateFromError를 선언하여, 자신 컴포넌트에서 에러가 발생시 이를 감지하여 반환하도록 하였습니다.
또한 에러의 상태를 초기화하는 reset과 초기값을 설정하는 생성자인 c constructor를 호출하고 있습니다.
LogicErorBoundary의 경우 에러 관련 추가적인 처리가 필요한 경우에 처리를 담당하고 있습니다.
예를 들어, 저희는 accessToken의 유효기간이 만료되면 다시 서버에 재로그인 요청을 보냅니다. 이런 요청을 처리하는 것을 LogicErrorBoundary에서 처리해주게 됩니다.
FallbackErrorBoundary의 경우 이름 그대로 대체 화면을 보여주는 역할을 맡고 있습니다. 해당 경우 여타의 처리를 할 필요 없이 화면을 바로 보여주면 되기 때문에 로직에러바운더리와 성격이 달라 분리하게 되었습니다.
FallbackErrorBoundary의 경우 서버에서의 에러 발생시 서버에러 페이지를 보여주는 것과 유저가 url을 잘못입력할 경우, 서버에 잘못된 요청이 가게되어 NotFoundError페이지를 보여주는 것으로 로직이 끝난 경우에 대해서 대응하고 있습니다.