Notice
Recent Posts
Recent Comments
Link
«   2026/07   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

mimizae 님의 블로그

에러 핸들링을 하는 방법(Suspense와 ErrorBoundary) 본문

FE

에러 핸들링을 하는 방법(Suspense와 ErrorBoundary)

mimizae 2026. 1. 2. 00:21


에러 핸들링이란 무엇인가

에러 핸들링은 단순히 에러를 처리한다는 의미가 아니라 UX의 일부이다.

 

사용자가 서비스를 이용하는 모든 경우의 수가 개발자와 기획자의 의도에 맞게 잘 흘러가면 좋겠지만... 에러와 예외는 어쩔 수 없이 늘 발생한다.  그 이유는 너무도 다양하다! 서비스 실행 환경, 개발자의 실수, 의도된 에러, 사용자의 특성 등등... 다양한 원인들이 있기에 에러와 예외 발생율이 0%이 될 수는 없다.

 

즉, 개발자는 사용자가 서비스를 이용하는 동안 발생할 수 있는 에러를 사전에 예측하여, 사용자의 서비스 이용 흐름이 막히지 않도록 유도해야한다. 이런 과정을 에러 핸들링이라 한다.

에러 핸들링을 함으로써 사용자에게 서비스가 다시 안정화될 것이라는 믿음을 주고, 사용자의 이탈을 방지할 수 있다.


에러 핸들링의 종류

위에서 살짝 언급했는데, 에러라는 건 사실 Error(에러) Exception(예외) 두 가지로 나눌 수 있다.

즉, 에러 핸들링 자체도 에러 핸들링(Error Handling)예외 핸들링(Exception Handling) 두 가지로 구분할 수 있다.

 

에러와 예외는 비슷해 보이지만, 발생 주체와 발생 상황이 다르다. 쉽게 말해, 에러는 시스템이나 환경에 의해 발생하는 것이고, 예외는 개발자가 의도적으로 발생시키는 것이다.

 

에러 핸들링은 보통 다음과 같은 상황에서 필요하다.

  • 네트워크 통신 실패로 데이터를 받아오지 못한 경우
  • 서버 내부 오류(500 에러 등)로 요청이 정상 처리되지 않은 경우
  • 예상하지 못한 런타임 오류로 애플리케이션이 중단될 수 있는 경우

반면, 예외 핸들링은 아래와 같은 상황에서 사용된다.

  • 필수 파라미터가 누락된 요청을 보냈을 때
  • 인증되지 않은 사용자가 접근 권한이 없는 리소스를 요청했을 때
  • 비즈니스 로직상 허용되지 않은 상태에서 특정 행동을 시도했을 때

에러 핸들링과 예외 핸들링의 차이는 결국 예측 가능성에 있다.

 

앞선 예시에서 볼 수 있듯, 에러(Error) 는 네트워크 장애나 서버 오류처럼 개발자와 사용자 모두가 사전에 정확히 예측하기 어려운 상황에서 발생한다. 언제, 어떤 조건에서 발생할지 알 수 없기 때문에 애플리케이션의 흐름을 갑자기 끊어버릴 수 있고, 제대로 처리하지 않으면 서비스 전체의 안정성을 해칠 수도 있다.

 

반면 예외(Exception) 는 대부분 예측 가능한 상황이다.
필수 값이 누락된 요청, 인증되지 않은 접근, 비즈니스 규칙을 벗어난 행동 등은 서비스 설계 단계에서 충분히 예상할 수 있는 경우에 해당한다. 그렇기 때문에 개발자는 이러한 상황을 미리 정의하고, 의도적으로 예외를 발생시켜 흐름을 제어한다.

즉, 예외 핸들링은 발생하면 안 되는 상황을 막기 위한 장치라면, 에러 핸들링은 언제든 발생할 수 있는 문제에 대비하기 위한 안전망에 가깝다.

 

그렇다면 각각의 상황에서 우리는 어떻게 대처해야 할까?

 

먼저 예외(Exception)의 경우, 개발자가 의도적으로 발생시킨 상황이기 때문에 비교적 대응 방식이 명확하다.

잘못된 방식으로 서비스가 사용되기 전에 예외 상황을 미리 정의하고 에러를 발생시켜, 서버는 이를 에러 코드와 함께 클라이언트(프론트엔드)에게 전달한다. 프론트엔드 개발자는 해당 에러 코드나 메시지를 기반으로 사용자에게 적절한 안내 문구를 노출해 주면 된다.

 

즉, 예외 핸들링의 핵심은 사용자가 올바른 흐름으로 서비스를 이용할 수 있도록 유도하는 것이다.

 

그렇다면 에러(Error) 는 어떨까?

에러는 사용자나 개발자가 의도하지 않은, 예기치 못한 상황에서 발생한다. 앞서 살펴 봤듯 매우 다양한 원인이 있고, 개발자는 이를 고려해 최대한 서비스가 안정적으로 동작하도록 대응해야 한다.

 

여기까지 정리하면, 우리가 프론트엔드에서 주로 고민해야 할 대상은 자연스럽게 Error(에러) 쪽이라는 걸 알 수 있다.
그리고 이 에러가 실제 서비스에서 가장 빈번하게 발생하는 지점은 대부분 서버와의 통신 과정이다.

 

서버 요청은 네트워크 상태, 서버의 처리 시간, 인증 여부 등 수많은 변수에 영향을 받으며, 무엇보다 응답 시점을 예측할 수 없다.
그래서 브라우저는 서버 요청을 비동기(async) 방식으로 처리하고, 그 결과 프론트엔드에서 마주치는 에러 역시 대부분 비동기 요청이 실패하는 순간에 발생한다.

 

이 지점에서 에러 핸들링은 단순히 에러를 잡는다는 의미를 넘어선 다는 것을 느낄 수 있다.
비동기 환경에서 에러를 잘 처리한다는 것은, 실패를 숨기는 것이 아니라 사용자에게 지금 어떤 상태에 놓여 있는지를 UI로 명확히 보여주는 것에 가깝다.

 

여기서 말하는 상태는 다음 두 가지로 볼 수 있다.

  1. 요청은 시작됐지만 아직 결과가 도착하지 않은 상태
  2. 요청이 끝났고, 그 결과가 실패로 돌아온 상태

이 상태들을 각각 다루는 방법으로는 isLoading, isError 같은 상태 값을 두고 UI를 조건부 렌더링하는 방식이 가장 먼저 떠오를 것이다.

 

아래 코드 예시를 봐보자.

function ExampleComponent() {
  const [data, setData] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [isError, setIsError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const result = await getData();
        setData(result);
      } catch (e) {
        setIsError(e as Error);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, []);

  if (isLoading) return <p>Loading...</p>;
  if (isError) return <p>에러가 발생했습니다.</p>;

  return <div>{data}</div>;
}

 

물론 변수를 통한 방법도 하나의 처리 방식이 될 수는 있다!


하지만... 이 방식은 데이터를 가져오는 로직과, 로딩 상태 관리, 에러 처리, 그리고 UI 렌더링 로직이 하나의 컴포넌트 안에 모두 섞여 있다.

컴포넌트는 점점 비대해지고, 비동기 흐름을 이해하려면 여러 상태를 동시에 추적해야 한다... ☠️

또한 로딩과 에러에 대한 처리 방식이 컴포넌트마다 반복되면서 코드의 재사용성은 떨어지고, 추상도는 낮아진다.

 

결국 데이터를 보여주는 컴포넌트임에도 불구하고, 비동기 상태를 관리하기 위한 코드가 핵심 로직을 가려버리는 셈...!!!

 

그렇다면 비동기 에러를 다룰 현명한 해결책은 무엇일까? 명령형으로 상태를 하나하나 관리하는 대신, 선언적으로 가보자!!


Suspense의 활용

먼저, 요청은 시작됐지만 아직 결과가 도착하지 않은 상태를 선언적으로 해결해 보자!!

 

React 공식 문서에 따르면, Suspense자식 요소를 로드하기 전까지 화면에 대체 UI(Fallback)를 보여주는 도구이다.

이는 단순히 로딩 중이라는 상태를 보여주는 역할이 아니라, 렌더링 흐름 자체에 비동기 상태를 선언적으로 표현할 수 있게 해준다.

 

기존 React 렌더링은 한 번 시작되면 멈출 수 없는 단일 스레드 기반이기에, 비동기 데이터가 아직 도착하지 않은 상태까지도 직접 제어해야 했다.

이게 무슨 말이냐면, React는 JS 위에서 동작하고 JS는 single thread로 동작한다. 즉, React는 렌더링 동작이 일어나고 있을 때 main thread를 점령하게 되고, 이 때문에 진행 중인 작업이 다 완료되어야지만, user event와 같은 interaction에 대한 작업을 수행할 수 있다.

 

이런 방식은 복잡한 비동기 UI를 구축할 때 필연적으로 많은 상태 관리 코드와 조건부 분기 로직을 요구한다. 이것은 React가 선언형을 지향하는 철학과는 다소 거리가 있었다...!!! 😅

 

이를 해결하기 위해 React는 v18 기준으로 렌더링 과정을 더 유연하게 설계할 수 있는 Concurrent Features를 도입했으며, 그 일부로 Suspense가 확장되었다. 과거에는 lazy 로딩 같은 코드 분할에만 적용되었지만, 이제는 데이터 페칭과 같은 비동기 작업도 Suspense 경계를 통해 자연스럽게 처리할 수 있도록 설계되었다.

 

바로 Suspense를 활용한 예시로 수정해 보며 왜 React의 Suspense를 쓰면 코드가 선언적이게 되고, isLoading과 같은 변수를 통해 UI를 제어하는 방식의 단점이 보완이 되는지 한 눈에 이해해 보자. 

// ExampleComponent.tsx
function ExampleComponent() {
  const { data } = useSuspenseQuery({
    queryKey: ["example"],
    queryFn: getData,
  });

  return <div>{data}</div>;
}
export default ExampleComponent;

// App.tsx
function App(){
 return(
  <Suspense fallback={<p>Loading...</p>}>
   <ExampleComponent />
  </Suspense>
 )
}

export default App;

 

이 코드가 바로 위의 방식을 수정한 결과다!!! (딱 봐도 정돈된 게 느껴진다.)

 

Suspense를 사용하니 ExampleComponent 컴포넌트는 더 이상 로딩 상태를 직접 관리하지 않는다.

대신 데이터가 준비되었을 때 어떤 화면을 그릴 것인가!! 만 선언하고 로딩 중인지 아닌지에 대한 판단 React와 Suspense가 담당하게 된다.

 

function ExampleComponent() {
  const { data } = useSuspenseQuery({
    queryKey: ["example"],
    queryFn: getData,
  });

  return <div>{data}</div>;
}

 

사실 이 코드만 놓고 보면, 로딩 상태가 보이지 않아서 음... 아예 비동기 상태가 사라진 건가?? 🤔 할 수 있다.

하지만 여기에서 포인트는!!!! 비동기 상태를 컴포넌트 내부의 변수로 관리하느냐, 아니면 렌더링 흐름에 위임하느냐에 있는 것이지, 사라진 건 절대 아니라는 것이다.

 

그렇다면 이 위임은 정확히 어떻게 이루어지는 걸까?
그리고 수정된 코드를 보면 useSuspenseQuery라는 게 등장했는데, 왜 useSuspenseQuery가 필요한 걸까?

이를 이해하려면 Suspense(useSuspenseQuery)의 동작 방식을 살펴볼 필요가 있다.


Suspense의 동작 방식 (useSuspenseQuery)

ExampleComponent가 렌더링되는 시점에 useSuspenseQuery는 먼저 queryKey를 기준으로 캐시를 확인한다.

이미 데이터가 있다면 그대로 값을 반환하고 렌더링은 정상적으로 진행된다. 반대로 데이터가 없거나 stale 상태라면 queryFn을 실행해 서버 요청을 시도한다. 여기서 기억해야 할 것은 이 서버 요청이 비동기 작업이라는 점이다.


렌더링 시점에는 아직 결과를 알 수 없기 때문에, 이 상태를 단순히 undefined나 null로 반환해 버리면 React는 렌더링을 계속 진행해도 되는 상태로 오해하게 된다.

 

하지만... 여기서 TanStack Query는 다른 선택을 한다!!! 데이터가 준비되지 않은 순간, 값을 반환하는 대신 Promise를 throw한다.

 

이 throw는 단순한 예외 처리가 아니다. 렌더링 중에 Promise가 throw되면, React는 이를 아직 이 컴포넌트는 렌더링할 준비가 되지 않았다는 신호로 해석한다. 그래서 React는 해당 컴포넌트의 렌더링을 잠시 중단하고, 가장 가까운 <Suspense> 경계로 제어를 넘긴다.

 

그럼 Suspense는 이 Promise를 잡아 resolve 될 때까지 fallback으로 전달된 UI를 렌더링한다.

return(
 <Suspense fallback={<p>Loading...</p>}>
   <ExampleComponent />
 </Suspense>
)
 

Promise가 해결되면 React는 다시 렌더링을 시도하고, 그 시점에는 useSuspenseQuery가 실제 데이터를 반환하게 된다.

이 과정을 통해 로딩 UI는 컴포넌트 외부로 자연스럽게 분리된다.

 

이 구조에서 ExampleComponent 컴포넌트는 더 이상 로딩 중일 때 무엇을 보여줄지를 고민하지 않는다!! 단지 데이터가 준비되었을 때의 UI만을 선언한다.

 

이것이 Suspense를 사용했을 때 코드가 더 선언적으로 보이는 이유다. 상태 값을 하나하나 체크하며 UI를 분기하는 대신, 아직 준비되지 않았다면 여기서 멈추고 대체 UI를 보여달라는 의도를 React에게 맡기는 것이다!


ErrorBoundary의 활용

그렇다면 이제, 요청이 실패했을 때 발생하는 에러는 어디에서 어떻게 처리되는 걸까?

// ExampleComponent.tsx
function ExampleComponent() {
  const { data } = useSuspenseQuery({
    queryKey: ["example"],
    queryFn: getData,
  });

  return <div>{data}</div>;
}
export default ExampleComponent;

// App.tsx
function App(){
 return(
  <Suspense fallback={<p>Loading...</p>}>
   <ExampleComponent />
  </Suspense>
 )
}

export default App;

 

아까 원본 ExampleComponent에서는 error라는 상태 값을 두고 서버 요청이 실패한 경우를 직접 처리했다.
하지만 Suspense를 도입한 지금의 코드에서는 에러를 다루는 로직 역시 컴포넌트 내부에서 완전히 사라져 있다!!

 

그럼 에러는 어디에서 잡아서 처리하는 거지...? 🤔 이때 사용하는 게 ErrorBoundary!!

 

ErrorBoundary는 렌더링 과정에서 발생한 JavaScript 에러를 포착해, 전체 앱이 무너지는 대신 대체 UI(fallback UI)를 보여주도록 도와주는 React의 메커니즘이다.

 

즉, ErrorBoundary의 목적은 단순히 에러를 잡는 것이 아니라 요청이 실패했을 때 화면이 깨지거나 흰 화면이 보이지 않도록 사용자가 인지 가능한 상태로 UI를 보여주는 것이다.

 

그럼 ErrorBoundary는 모든 에러를 만능으로 잡을까?

 

React 공식 문서에서는 다음과 같이 명시하고 있다.

에러 경계는 아래의 경우에 발생하는 에러들을 잡지 않습니다.
  • 이벤트 핸들러 내부에서 발생한 에러
  • 비동기 코드 (예: setTimeout, requestAnimationFrame, Promise의 then 내부)
  • 서버 사이드 렌더링 중 발생한 에러
  • 에러 바운더리 자체에서 발생한 에러

즉, ErrorBoundary는 렌더링 과정에서 발생한 에러만을 대상으로 한다.

 

이 점을 이해하지 못하면 왜 ErrorBoundary로 감쌌는데도 에러가 안 잡히지?!라는 혼란을 겪기 쉽다.

여기, 못 잡는 에러에 대한 부분은 추후에 다루기로 하고, 우선 ErrorBoundary를 적용하는 방법부터 살펴보자!

 

이는 직접 클래스로 구현을 하거나 react-error-boundary라는 라이브러리를 사용하는 방법 두 가지가 있다.


ErrorBoundary 구현 방법

1) 클래스 컴포넌트

React 공식 문서에서 제시하는 ErrorBoundary의 기본 형태는 클래스 컴포넌트다.

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

  static getDerivedStateFromError(error) {
    // 다음 렌더링에서 폴백 UI가 보이도록 상태를 업데이트 합니다.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // 에러 리포팅 서비스에 에러를 기록할 수도 있습니다.
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // 폴백 UI를 커스텀하여 렌더링할 수 있습니다.
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children;
  }
}

 

이 구현에서 핵심은 두 가지 생명주기 메서드다.

 

1. getDerivedStateFromError

이 메서드는 자식 컴포넌트에서 에러가 발생했을 때 호출되는 정적 메서드다. 여기서 반환한 값은 다음 렌더링에 반영될 state가 된다.

 

즉, 에러가 발생하는 순간 React는

  1. getDerivedStateFromError를 호출하고
  2. hasError: true로 상태를 업데이트한 뒤
  3. 다시 렌더링을 시도한다

그 결과 render 메서드 안의 조건문이 동작하며, 정상 UI 대신 fallback UI가 화면에 나타나게 된다.

 

2. componentDidCatch

 

componentDidCatch는 에러가 실제로 발생한 이후, 사이드 이펙트를 처리하기 위해 호출되는 메서다.

 

여기서는 보통 다음과 같은 작업을 수행한다.

  • 에러 로그를 콘솔에 출력
  • Sentry, Datadog 같은 에러 리포팅 서비스로 전송
  • 디버깅을 위한 추가 정보 수집

중요한 점은 이 메서드가 UI를 바꾸기 위한 용도가 아니라는 것이다!!!

UI 전환은 getDerivedStateFromError가 담당하고, componentDidCatch는 오직 에러 관측과 기록에 집중한다.

 

이 방식은 React에서 ErrorBoundary를 구현하는 정석적인 방법이지만, 현재의 함수형 컴포넌트 중심 React 코드베이스에서는 다소 부담스럽게 느껴질 수 있다...

 

(클래스 컴포넌트를 직접 작성해야 하고 생명주기 메서드를 이해해야 하며 상태 관리 로직까지 직접 구현해야 하기 때문!!!!!)

2) react-error-boundary

이 문제를 해결하기 위해 등장한 라이브러리가 react-error-boundary다!!

이 라이브러리는 ErrorBoundary를 함수형 컴포넌트 + 훅 기반 API로 사용할 수 있게 해준다.

import { ErrorBoundary } from "react-error-boundary";

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>문제가 발생했습니다.</p>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}
function App() {
  return (
    <ErrorBoundary
      FallbackComponent={ErrorFallback}
      onReset={() => {
        // 쿼리 리셋, 상태 초기화 등
      }}
    >
      <Suspense fallback={<p>Loading...</p>}>
        <ExampleComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

 

ErrorBoundary 컴포넌트가 제공하는 주요 props를 살펴보자.

 

1. FallbackComponent

return(
 <ErrorBoundary FallbackComponent={ErrorFallback}>
   {children}
 </ErrorBoundary>
)

// 대체 UI 컴포넌트
function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div>
      <p>에러가 발생했습니다.</p>
      <pre>{error.message}</pre>
      <button onClick={resetErrorBoundary}>다시 시도</button>
    </div>
  );
}

 

에러가 발생했을 때 렌더링할 대체 UI 컴포넌트지정한다.

 

이 컴포넌트는 자동으로 다음 props를 전달받는다.

  • error → 실제로 발생한 에러 객체 (에러 메세지 뽑아내기)
  • resetErrorBoundary → 에러 상태를 초기화하는 함수

덕분에 fallback UI에서 에러 메시지를 보여주거나, 재시도 버튼을 쉽게 만들 수 있다!

 

2. fallback / fallbackRender

return(
 <ErrorBoundary fallback={<p>에러가 발생했습니다.</p>}>
   {children}
 </ErrorBoundary>
)

//혹은 함수 형태로

return(
 <ErrorBoundary
  fallbackRender={({ error, resetErrorBoundary }) => (
    <div>
      <p>{error.message}</p>
      <button onClick={resetErrorBoundary}>재시도</button>
    </div>
  )}
 >
  {children}
 </ErrorBoundary>
)

 

FallbackComponent 대신 더 간단한 방식도 제공한다.

  • 단순한 UI → fallback
  • 로직이 필요한 경우 → FallbackComponent or fallbackRender

상황에 따라 선택할 수 있다!

 

3. onReset

return(
 <ErrorBoundary
  FallbackComponent={ErrorFallback}
  onReset={() => {
    // 상태 초기화, 쿼리 리셋 등
  }}
 >
  {children}
 </ErrorBoundary>
)

 

resetErrorBoundary가 호출되면 이 onReset 콜백이 함께 실행된다. (재시도 버튼의 onClick 이벤트에 주로 쓰임)

이건 특히 TanStack Query와 함께 사용할 때 매우 중요하다.

  • 실패한 쿼리 캐시 초기화
  • 전역 상태 리셋
  • 특정 파라미터 초기화

같은 작업을 이 지점에서 처리할 수 있다!!

 

4. resetKeys

return(
 <ErrorBoundary resetKeys={[userId]}>
   {children}
 </ErrorBoundary>
)

 

resetKeys는 값이 변경되면 ErrorBoundary의 에러 상태를 자동으로 초기화한다.

예를 들어!!

  • 다른 유저 페이지로 이동했을 때
  • 다른 ID의 데이터를 요청할 때
  • 검색 조건이 변경되었을 때

이전 에러 상태를 끌고 가지 않고 새로운 시도로 자연스럽게 전환할 수 있다.

(이건 직접 구현하면 은근히 까다로운 로직일 듯한데, react-error-boundary에서는 prop 하나로 해결된다!)

 

5. onError

return(
 <ErrorBoundary
  onError={(error, info) => {
    logErrorToService(error, info);
  }}
 >
  {children}
 </ErrorBoundary>
)

 

에러가 발생했을 때 호출되는 콜백이다.

  • 에러 로깅
  • 모니터링 서비스 연동
  • 추가적인 디버깅 정보 수집

등을 이곳에서 처리할 수 있다.

클래스 컴포넌트의 componentDidCatch 역할을 그대로 대체한다고 보면 된다!!!


useErrorBoundary 훅

위에서, ErrorBoundary는 비동기 코드 내부의 에러를 자동으로 잡아주지 않는다고 했다.

왜일까? 이건 React의 설계 때문이다.

비동기 로직(fetch, then, catch)은 이미 렌더링이 끝난 뒤, React의 렌더링 컨텍스트 밖에서 실행된다!!!

 

그래서 아래와 같은 코드는 작동하지 않는다.

getData().catch(error => {
  throw error; // ❌ ErrorBoundary가 못 잡는다
});

 

아니 그럼!! 비동기 에러는 어떻게 해야 하지...??? ☠️☠️☠️

 

 react-error-boundary는 여기에서 useErrorBoundary라는 해결책을 제공한다.

이 훅은 비동기 에러를 ErrorBoundary가 처리할 수 있는 에러로 승격 시킨다.

 

useErrorBoundary 훅을 사용하면 비동기 코드에서 발생한 에러를 의도적으로 ErrorBoundary로 전달할 수 있다!!!!

function ExampleComponent() {
  const { showBoundary } = useErrorBoundary();

  const { data } = useSuspenseQuery({
    queryKey: ["example"],
    queryFn: async () => {
      try {
        return await getData();
      } catch (error) {
        showBoundary(error);
        throw error; // 타입/흐름 정리를 위해
      }
    },
  });

  return <div>{data}</div>;
}

 

아까 본 Suspense 예제에 useErrorBoundary을 활용한 에러 처리를 붙이면 이렇게 된다!!!


정리

결국 우리가 집중해야 할 대상은 예외(Exception) 보다는 에러(Error) 다. 그리고 그 에러는 대부분 비동기 방식으로 처리되는 서버 요청이 실패하는 순간에 발생한다.

이 에러를 잘 핸들링한다는 것은, 단순히 콘솔에 에러를 찍거나 실패를 감추는 일이 아니다. 사용자에게 지금 애플리케이션이 어떤 상태에 놓여 있는지를 UI로 명확하게 보여주는 것에 가깝다.

 

이때 우리가 마주하는 상태는 크게 두 가지다.

  • 요청은 시작됐지만 아직 결과가 도착하지 않은 상태
  • 요청이 끝났고, 그 결과가 실패로 돌아온 상태

Suspense는 이 중 첫 번째 상태, 즉 아직 결과가 준비되지 않은 상태를 선언적으로 표현한다. 로딩 여부를 변수로 추적하는 대신, 여기서는 아직 멈춰야 한다는 의도를 렌더링 흐름에 위임한다.

 

반면 ErrorBoundary는 두 번째 상태, 즉 요청이 끝났지만 실패한 경우를 책임진다. 화면이 무너지거나 흰 화면이 보이는 대신, 실패했음을 인지 가능한 UI로 안전하게 드러내는 역할이다.

그리고 ErrorBoundary가 직접 다루지 못하는 비동기 코드 내부의 에러는 react-error-boundary의 useErrorBoundary를 통해 다시 렌더링 흐름으로 끌어올 수 있다.

 

즉, 비동기 에러를 안정적으로 핸들링한다는 것은 하나의 도구로 해결하는 문제가 아니라,
Suspense와 ErrorBoundary, 그리고 useErrorBoundary의 조합을 통해 각각의 상태를 올바른 책임 위치에서 선언적으로 표현하는 것이다. 이렇게 역할이 분리된 구조 안에서, 에러는 더 이상 예외적인 사고가 아니라 의도적으로 다뤄지는 UI 상태가 된다!!

 

💭 찾아보며 헷갈렸던 점 (내용이 길어서 접은 글로 추가)

더보기

서버 통신이 있는 모든 컴포넌트에 Suspense를 적용해야 할까?

 

공부를 하면 할 수록 "오? 서버 통신이 있는, 그러니까 비즈니스 로직이 있는 모든 컴포넌트는 Suspense와 Errorboundary로 감싸야 하는 건가?" 하는 생각이 들었다.

 

이걸 보는 여러분들도 '에러가 발생할 수 있는 모든 컴포넌트를 저 패턴으로 감싸야 하는 건가?' 라는 생각이 들 수 있을 것 같다.

 

결론부터 말하자면 그렇지 않다!!

만약 모든 컴포넌트를 그렇게 감싸고 있다면, 그건 오히려 Suspense와 ErrorBoundary를 잘못 쓰고 있는 신호일 가능성이 크다.

 

왜 모든 컴포넌트에 적용하면 안 될까?

Suspense와 ErrorBoundary는 비동기 요청을 감싸기 위한 도구가 아니라, 비동기 요청의 결과가 렌더링에 반영되는 지점을 다루는 도구다.

💭 아니 에러는 비동기 방식으로 처리하는 서버 요청에서 자주 일어나고, 이 에러 상태를 처리하기 위해 저 패턴이 자주 사용된다고 한 거 아니냐?!!! 라고 할 수 있다. 

 

'Suspense와 ErrorBoundary, 그리고 useErrorBoundary의 조합을 통해 각각의 상태를 올바른 책임 위치에서 선언적으로 표현하는 것이다.'

라고 정리 부분에서 말한 것처럼 중요한 건 에러가 어디에서 발생하느냐가 아니라, 그 에러를 어디에서 보여줄 것인가다.

 

즉, 기준은 이 컴포넌트가 서버 요청을 하느냐가 아니라 이 실패를 하나의 UI 상태로 사용자에게 보여줘야 하는 경계인가?!다.

 

그래서 서버 통신이 여러 곳에서 발생하더라도 그 결과가 하나의 페이지, 하나의 섹션, 하나의 카드 묶음으로 표현된다면 Suspense와 ErrorBoundary는 그 가장 바깥의 의미 있는 경계에만 두는 것이 바람직하다.


참고 자료

[React] react-error-boundary로 에러 핸들링하기

React 공식문서 <Error Boundaries>

React Suspense는 왜 등장했나

TanStack Query 공식문서, useSuspenseQuery

React 공식문서 <Suspense>

React Suspense: 리액트 서스펜스를 사용하는 이유와 사용법 총정리

ErrorBoundary & Suspense, 거의 완벽한 사용방법 가이드

What is Error Handling?

'FE' 카테고리의 다른 글

swagger-typescript-api 적용기  (0) 2026.01.28
pnpm이란?  (0) 2026.01.02
CI/CD란 뭘까?  (0) 2025.12.26
페이지네이션에 대해서  (0) 2025.12.21
Tanstack Query의 데이터 최신화  (0) 2025.12.14