Profile Picture

윤찬의 개발노트

2025. 2. 20.

Error Boundary를 실용적으로

VienceException
"어디까지 에러로 정의할까"

리팩토링 기간중 도입하게된 에러처리

서비스 리팩토링을 진행하면서, 평소엔 그냥 지나쳤던 에러 처리에 대해 다를 수 있게 되었다. axios response로 받고 예외를 정의하는 과정에서 매번 처리를 해야한다는 번거러움과 당시는 swal 라이브러리를 사용해 알림을 띄우고 있었는데, 이 방식은 모든 api에 일관되게 적용되지 않았고, 어떤 api에서는 아예 에러 처리가 빠져 있는 경우도 있었다.

✔️ 에러 처리가 어려웠던 이유들

이런 상황까지 오게된 배경에는 여러 가지 복합적인 요인이 있었다.

먼저, api의 수가 너무 많았다. 각 페이지마다 독립적인 api 요청이 수십 개씩 존재했고, 소수의 개발자로 구성된 팀에서는 매 요청마다 예외 처리를 꼼꼼하게 작성하는 게 현실적으로 쉽지 않았다. 비즈니스 로직을 구현하는 것만으로도 벅찼기 때문에, 에러 처리에는 항상 우선순위가 밀릴 수밖에 없었다.

또한, 기능 간 의존성이 높은 구조도 문제였다. 단일 서비스라고 하더라도 내부적으로는 여러 기능이 서로 다른 서비스처럼 쪼개져 있었고, 이 기능들이 복잡하게 연결되어 순차적으로 실행되는 경우가 많았다. 예를 들어, A 기능이 성공적으로 실행되어야 B 기능이 동작할 수 있는 구조인데, A에서 발생한 에러가 전체 흐름을 막아버리는 일이 자주 있었다. 이럴 때는 문제의 원인을 파악하는 것도 어렵고, 개발자나 사용자 입장에서는 기능이 안 되는 이유를 전혀 알 수 없었다.

테코를 통해 사전에 방지할 수는 있지만 스업 특성상, 모든 흐름마다 테스트 코드를 계속 작성하는 것이 항상 가능하지는 않았다. 테코도 결국 "자원"이라고 볼 수 있기 때문에, 어떤 경우에는 이를 매번 적용하는 것이 오히려 비효율적일 수 있었다.

뿐만 아니라, 일부 컴포넌트는 데이터를 받아오는 데 시간이 오래 걸리는 비동기 작업이 포함되어 있었는데, 예를 들어 이미지가 자동으로 넘어가는 슬라이드 컴포넌트에서는 백단에서 큰 데이터를 받아와야 했고, 해당 요청이 실패하거나 지연되면 전혀 관련 없어 보이는 다른 UI 컴포넌트에도 영향이 가는 상황이 계속 발생했다. 이 역시 각 컴포넌트 단위로 에러를 캡처해서 처리하지 않는 구조에서 발생한 문제였다.

마지막으로, 당시엔 유저 테스트 기간이기도 했기 때문에 상황은 더 복잡해졌다. 일부 기능은 테스트 중이라 일시적으로 비활성화되어 있었고, MVP 목적에 따라 해당 API 요청을 중단하거나 실패하도록 구성해두었다. 문제는 이런 상태에서도 사용자에게 자연스럽게 안내하거나, 실패했을 때 UI가 망가지지 않도록 하려면 별도의 예외 처리가 필요했지만, 해당 기능이 언제 다시 열릴지 알 수 없기 때문에 처리 기준이 모호해지고 애매한 상황이 자주 발생했다는 것이다.

🎯 에러의 확실한 처리가 필요

이처럼 에러 처리의 필요성은 위와같이 분명했지만, 매번 try-catch를 쓰는 방식이나 각 컴포넌트 내부에서 에러를 따로 관리하는 방식은 유지보수 측면에서 한계가 있었다. 그래서 이번 리팩토링 기간을 계기로, React의 Error Boundary를 도입하기로 했다.


일반적인 ERROR BOUNDARY

공식문서의 Error Boundary 정의

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, erro	rInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }

Error Boundary를 통해 예상치 못한 렌더링 중 에러가 발생했을 때, 전체 앱이 망가지지 않고 문제 있는 컴포넌트만 fallback UI로 대체되도록 구성한다. 이렇게 된다면 사용자는 최소한의 안내 메시지를 받을 수 있게 되었고, 개발자 입장에서도 어떤 컴포넌트에서 에러가 발생했는지를 빠르게 파악할 수 있게 되었습니다.특히, 공통적인 에러 처리 로직을 바운더리 내부에 구성해두면서 반복되는 코드도 줄이고 개선할 수 있다.

물론, 이렇게 Error Boundary를 일반적으로 활용하는 것도 좋지만, '틀을 만든다'는 것은 동시에 커스터마이징의 가능성도 열린다는 의미라고 생각한다. 단순히 공식 문서에 있는 예제를 그대로 사용하는 것도 가능하지만, 우리 팀 내부에서는 조금 더 재미있고 실용적인 방식으로 에러를 처리해보자는 관점에서 다시 한 번 고민해보게 되었다.

그래서 나온 방법은, 정말 재미있었는데 다음과 같다.

  • 에러 처리에 대한 코드를 모듈화 시키는 것
  • UX와 DX를 동시에 가져가는 방법
  • 브라우저의 에외를 잡는 것

팀에서 효율적으로 커스텀 한 과정

🎯 에러 처리에 대한 코드를 모듈화 시키는 것

각각의 api에서 발생할 수 있는 에러를 일관된 방식으로 다루면서, UI 컴포넌트의 상황에 맞게 구분해서 보여줄 수 있도록 설계를 했다. 예를 들어, 단순한 alert 메시지로도 충분한 api는 그냥 경고창 하나로 끝낼 수 있지만, 아직 response를 기다리고 있는 api 요청의 경우엔 단순한 알림보다는 로딩 상태를 보여주거나, 요청이 실패했을 때 재시도 안내를 포함한 모달 형태로 응답 상태를 표현할 수 있도록 분리를 한 것이다.

이렇게 적용이 된다면 단순히 "에러를 보여준다"는 것을 넘어서, 각 api의 상태와 역할에 맞는 사용자 경험을 설계할 수 있었다. 또한 코드 측면에서도, 수많은 API 중 어떤 API에 에러 바운더리가 적용되어 있는지 한눈에 파악할 수 있어, 코드 가독성과 유지보수성을 향상시켰다.


🎯 UX와 DX를 동시에 가져가는 방법

이부분은 재미있는곳에서 영감을 받았다. 예비군 훈련 중에 요즘 스마트 워치를 지급받았는데, 중간에 통신이 끊어졌을 때 화면에 개발자용 에러 메시지가 그대로 표시된 것이다. 물론 개발자 입장에서는 네트워크 연결 문제라고 바로 알 수 있었지만, 일반 사용자들은 이런 기술적인 메시지를 그대로 이해하기 어려울 수 있다.

그래서 생각한 방식은 사용자에게는 너무 기술적인 에러 메시지가 아닌, 그들이 이해할 수 있는 방식으로 오류를 전달하는 것이다. 예를 들어, 개발 모드에서는 어떤 API에서 에러가 발생했는지를 정확히 보여주는 것이 중요하고, 실제 서비스에서는 기능에 대한 오류 메시지를 사용자에게 안내하는 방식으로 분기 처리해야 한다고 생각했다.

사용자 시점

dev 단계


🎯 브라우저의 에외를 잡는 것

마지막으로, 현재 자원 특성상 많은 서버를 운영할 수는 없었고, 큰 이미지 처리와 같은 백엔드 작업은 실제로 극히 소수의 상황에서만 발생했다. 예를 들어, 리스트 API가 계속 pending 상태로 남아 있거나, 초기 API 호출에 실패하거나, 브라우저 자체에서 API 요청이 극히 드물게 실패하는 경우였다. 이런 상황에서는 모든 API에 대해 적용할 수는 없지만, 중요한 API들에 대해서는 2중 호출 방식으로 한 번 더 요청을 시도하거나, 재시도 로직을 적용하여 API에 대해 다시 요청을 보내는 방식으로 처리했다.

이때 중요한 점은 재시도할 API 요청을 정확히 구분하고, 재시도 로직을 효율적으로 관리하는 것이다. 이를 위해 retryKey를 이용해 retryRequestMap에서 해당 API에 대한 재시도 함수를 가져와 호출할 수 있도록 설계했다. 이렇게 하면 에러가 발생한 API에 대해서만 재시도 로직을 적용하고, 중요한 API들이 실패했을 때 사용자가 계속해서 원활하게 서비스를 이용할 수 있도록 할 수 있다.


최종 적용과 마무리

사실 리팩토링 기간에 짧게 Error Boundary만 적용하고 넘어가려 했지만, 욕심이 생겨 더 기능을 추가하게 되면서 이까지 온 것 같다. 최종적인 결과는 그래서 아래와 같은데, 나름 만족하는 중이다.

Profile Picture

CHAN

과정은 복잡하되, 결과는 단순하게

Thank You for Visiting My Blog, Have a Good Day 😆
ⓒYoonchan Cho