#react, #error, #error-handling

react에서의 에러 처리

(자료 출처 : https://unsplash.com/@visuals)
  • 🔥 아직 고민할 점이 많은 문제입니다. 다양한 의견 남겨주세요
  • 🔥 에러 처리에 대해서 개인적인 생각들을 담았습니다.

🚫 에러, 어떻게 처리해야하나

이전부터 프론트 개발을 하면서 계속 고민했던 부분이다. 팀 프로젝트를 할 때 명확한 기준없이 에러 처리를 해왔었는데 에러 처리 코드가 일관성이 떨어졌을 뿐만 아니라 에러 관련 코드가 산발적으로 흩어져 리팩터링할 때 어떤 파일을 보아야 하는 지 헤매는 경우가 많았다.

이러다보니 일관된 에러 처리 코드에 관한 갈증이 높아졌다. 최근 새롭게 팀 프로젝트를 하게 되었는데 이번에야말로 에러에 대한 기준을 세워야겠다고 마음먹었고 실제 적용했던 내용을 글로 작성하게 되었다.

처음으로 깊게 고민해본 것이라 부족한 점이 많을 것이다. 비판적으로 봐주시길 바란다.

🎯 에러 처리. 무엇이 중요할까

나는 에러 처리 방법을 정하기에 앞서, 에러 처리에서 무엇이 중요한가? 를 정의하는 것이 우선 되어야 한다고 생각한다. 무엇이 중요한가에 대해 모두가 생각이 조금씩 다르겠지만 나는 다음과 같이 생각한다.

1. 에러가 발생해도 사용자가 앱을 사용하는데 최대한 지장이 없어야 한다.

에러가 발생해도 중대한 에러가 아닌 이상 최대한 지장이 없어야 한다. 예를 들어 사용자 프로필 정보를 가져오는 페이지가 있다고 생각해보자. 프로필 정보를 불러오던 중, 사용자의 나이 정보만을 불러오지 못했을 때 페이지 전체가 에러 페이지로 대체된다면 어떨까? 나이 정보 외에는 사용자의 정보를 보여줄 수 있었지만, 사용자는 아무런 정보를 보지 못할 것이다. 사용자가 정상적으로 앱을 이용할 수 있음에도 이용하지 못하도록 강제했기 때문에 사용자 경험을 떨어뜨린다.

2. 사용자는 에러 발생 사실을 인지할 수 있어야 한다.

어떠한 방식으로든 사용자는 에러 발생 사실을 인지할 수 있어야 한다고 생각한다. 예를 들어 상품의 정보를 보여주는 페이지가 있다고 생각해보자. 상품 정보를 대부분 정상적으로 불러왔지만, 상품의 제조 일자를 알려주는 부분만을 받아오지 못했을 때 아무런 알림이나 UI적인 표현 없이 조용히 실패한다면 어떨까? 앱을 이전부터 써왔던 사용자라면 제조 일자를 불러오지 못했다는 사실을 알 테지만 앱을 처음 접한 사용자는 원래부터 제조 일자는 표시되지 않는 것으로 인식할 것이다. 에러를 어떻게 사용자에게 알려야 하는지는 에러의 중요도에 따라서 다르다. 후에 다뤄보도록 하겠다.

🤔 에러 처리 방법에 대한 고민

에러 처리에서 중요한 점을 정의했다. 이를 바탕으로 에러 처리 방법을 생각해보자.

에러 전파를 최대한 막기

javascript에는 이벤트 버블링이라는 개념이 있다. 웹상에서 어떤 이벤트(ex 클릭)가 발생했을 때, 이벤트가 발생한 element가 가장 먼저 이벤트 처리에 대한 권한을 가진다. 해당 element는 이벤트를 처리한 후에 상위 element로 이벤트를 전파한다.

에러 전파도 이벤트 버블링과 비슷하다고 생각했다. 우리가 에러 처리를 할 때 자주 사용하는 try catch와 throw 구문은 에러가 발생한 곳에서 에러 처리에 대한 권한을 가장 먼저 갖고, 처리할 수 없다면 다른 곳으로 처리를 위임한다.

이벤트 버블링과 에러 전파는 큰 차이점이 있는데, 이벤트 버블링은 이벤트를 처리하고도 다른 곳으로 전파되지만 에러는 최대한 빨리 처리하고 다른 곳으로의 전파를 막아야 한다는 점이다. 나는 이런 생각을 바탕으로 에러가 전파되는 3가지의 레이어를 정의했다.

  1. 에러가 최초 발생한 함수 (보통 fetch를 사용하는 네트워크 요청 관련 함수)
  2. 1번 함수의 반환값을 표현하는 컴포넌트
  3. 2번 컴포넌트를 감싸는 에러 바운더리

위의 구조를 적용해봤을 때 에러가 최초 발생한 함수에서 에러 전파를 끊는 경우는 아직 없었다. 1번에서 모든 에러 처리를 하고 전파를 끊기에는 상황에 따른 유연성이 너무 부족했기 때문이다. 그래서 우리는 1번을 console.error() 정도의 역할을 맡도록 하고 에러를 2번으로 전파하였다. 2번에서 해당 에러에 대한 모든 처리가 가능하다면 3번으로 에러 전파를 하지 않고 끊어냈다. 마지막 3번 에러 바운더리로 에러가 전파될 경우 에러 폴백 컴포넌트를 대신 렌더링하여 에러 전파를 무조건 끊어낸다.

말로 하는 것보다 직접 보는 것이 훨씬 낫다. 위 구조를 바탕으로 만든 예제를 같이 보자.

실제 팀 프로젝트에서는 react-query를 사용하고 있지만, react-query를 사용하지 않고 유사한 방식으로 구현해보았다. 큰 어려움 없이 구현할 수 있었는데, react-query를 사용하지 않는 프로젝트에도 위의 구조를 충분히 적용할 수 있음을 알 수 있었다.

const { data: userProfile } = useUserProfile({
  onError: error => {
    console.log('alert : ', error.message);
  },
  useErrorBoundary: true,
});

예제에서 가장 핵심적인 부분은 위 코드라고 생각한다. 네트워크 요청으로 데이터를 받아오기 위해서 훅을 사용하고 있고, 컴포넌트에서 훅 에러 처리를 위해 onError 인자를 넘기고 있다. 그리고 에러 바운더리로 에러 전파가 필요하다면 useErrorBoundary를 true로 설정해준다.

중요도에 따라 에러 종류 정의하기

먼저 나는 에러의 중요도를 사용자에게 얼마나 적극적으로 알려야 하는가로 정의했다. 중요도가 높을수록 사용자에게 적극적으로 알려야하는 것이다. 나는 이러한 중요도에 따라 에러를 3가지로 분류했다.

  1. 에러가 발생한 컴포넌트만 에러 화면으로 대체하는 방식 (小 에러)
  2. alert, snackbar 등 사용자에게 강제로 보여주는 방식 (中 에러)
  3. 1번과 2번을 모두 사용하는 방식(大 에러)

앞서 언급한 에러 전파 구조에 이와 같은 분류를 적용했을 때, 컴포넌트에서는 中 에러만을 처리하고 에러 바운더리에서는 小 에러만을 처리하는 구조가 만들어졌다. 大 에러를 처리하기 위해서는 컴포넌트에서 中 에러를 처리하고 에러 바운더리로 throw만 하면 된다.

이러한 구조를 적용했을 때 아무런 기준 없이 에러를 처리하던 기존 방식보다 훨씬 에러를 분류하기도 좋았고 코드를 작성하기에도 명료했다.

📦 번외. 에러 메세지 매핑

여기서 추가로 고민해보면 좋을 사항이 있다. 백엔드로부터 받아온 에러 메세지를 어떻게 사용자에게 보여줄 것인지에 관한 내용이다. 백엔드로부터 받아온 에러 메세지를 그대로 사용자에게 보여줄 수도 있겠지만 이렇게 하는 경우 프론트에서 사용자에게 보여줄 메세지를 변경하려면 백엔드 팀원에게 부탁해야 할 것이다. 이 상황은 어색하게 느껴진다. 사용자와 맞닿아 있는 곳은 프론트이기 때문에 사용자를 위한 메세지는 프론트에서 관리하고, 프론트와 맞닿아 있는 곳은 백엔드이기 때문에 백엔드에서 보내주는 메세지는 프론트 개발자를 위한 메세지로 관리하는 것이 더 자연스러울 것이다. 그래서 나는 에러 응답에 에러 메세지와 에러 코드를 담아 보내도록 백엔드와 협의하였다. 예를 들어 다음과 같이 관리했다.

image

이를 토대로 백엔드에서 에러가 발생했을 시 프론트로 다음과 같은 에러 응답을 보낸다.

{
  "errorCode": "feed-001",
  "errorMessage": "존재하지 않는 피드입니다."
}

받아온 에러 메세지는 console.error()로 출력하고 에러 코드를 프론트 내의 에러 메세지 객체와 매핑하여 사용자를 위한 에러 메세지도 프론트에서 관리할 수 있도록 하였다.

마무리

이번 글에서는 react에서 어떻게 에러 처리를 하는 것이 좋을지에 대한 주제를 다뤄보았다. 글을 작성하면서 느낀 것은 어떤 문제에 대한 해결책을 찾기 위해서는 일단 문제에 대한 상세한 정의가 중요함을 느꼈다. 에러 처리 방법을 고민하기 전에 에러 처리의 정의를 내려야 하는 것처럼 말이다.

에러 처리에서 아직 부족한 점이 많지만, 처음부터 완벽할 순 없다. 앞으로 여러 팀에서 일을 해보고 경험을 쌓으면서 좀 더 효율적인 에러 처리 방법을 찾을 수 있다면 좋겠다.

참고