
React는 대표적인 선언형(Declarative) UI 프레임워크이다.
"지금 UI가 어떤 상태(state)인지 선언하면, React가 그 상태에 맞게 DOM을 만들어준다."
즉, “어떻게” UI를 렌더링할지가 아니라 “어떤 UI를 보여줘야 하는지”를 선언하기만 하면 된다.
return isLoading ? <Loading /> : <DataTable data={data} />;
isLoading 상태에 따라 보여줄 컴포넌트를 조건만으로 선언해 상태가 바뀌면 React가 적절하게 리렌더링한다. 선언형 프로그래밍은 복잡한 로직을 명시적으로 제어하지 않고, "무슨 상태냐에 따라 UI를 결정"하는 방식이다.
React 컴포넌트 트리에서 에러가 발생하면 렌더링이 중단되며, 해당 트리 아래의 컴포넌트 전체가 사라질 수 있다. 예를 들어, 자식 컴포넌트에서 네트워크 요청 중 문제가 생겨 예외가 발생하면 전체 화면이 깨져 사용자에게 하얀 화면(blank screen)을 보여줄 수 있다. 이는 React가 선언형 UI를 재구성할 수 없게 되는 상태다. 따라서, 에러를 UI의 상태 중 하나로 간주하여 선언적으로 처리할 수 있는 방법이 필요하다.
try/catch처럼 명령적으로 처리하지 않고 선언형으로 에러 UI를 정의할 수 있다. 사용자 경험을 해치지 않으며 유지보수도 용이해진다.try/catch로 앱 전체를 감싸는 것이 아니라, 에러가 날 수도 있는 범위를 명확히 지정하고, 그때 어떤 UI를 보여줄지를 선언적으로 결정하는 것이 선언형 UI의 에러 처리 방식이다.<ErrorBoundary fallback={<ErrorFallback />}>
<MyComponent />
</ErrorBoundary>
React의 기본 ErrorBoundary는 클래스 컴포넌트로 구현해야 한다. 아래는 대표적인 생명주기 메서드들이다.
static getDerivedStateFromError(error): 하위 자손 컴포넌트에서 에러가 발생했을 때 호출됨. 새로운 state를 반환해야 렌더링을 fallback으로 바꿀 수 있다.
componentDidCatch(error, info): 실제 에러가 발생한 후 호출되며, 로깅 등 부수효과(side effect)를 처리할 수 있다.
현대 React 개발 트렌드는 대부분 함수형 컴포넌트 + 훅(hooks)을 기반으로 한다. useState, useEffect, useQuery 같은 훅 패턴과 잘 어울리기 위해서는 에러 바운더리도 함수형 방식에 적합해야 한다. 이때 사용할 수 있는 것이 바로 react-error-boundary다.
react-error-boundary는 ErrorBoundary를 확장하여 개선된 기능을 제공하는 라이브러리로, 전역 에러 핸들링 및 커스텀 에러 UI를 간편하게 구현할 수 있게 해준다. 어느 위치에서든 에러를 감지하고 사용자에게 보여줄 fallback UI를 쉽게 지정하여 처리할 수 있다.
1. FallbackComponent 또는 fallbackRender
에러 발생 시 보여줄 UI를 전달하는 방식이다.
<ErrorBoundary
FallbackComponent={({ error, resetErrorBoundary }) => (
<div>
<p>{error.message}</p>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
)}
>
<MyComponent />
</ErrorBoundary>
FallbackComponent는 에러 객체와 리셋 함수(resetErrorBoundary)를 인자로 받는다. fallbackRender는 위와 같은 함수를 인라인으로 바로 작성할 수 있도록 한 prop이다.
2. resetKeys와 onReset
resetKeys는 특정 prop 값이 변경되면 에러 상태를 자동으로 리셋한다. onReset은 수동 리셋 시 실행할 콜백이다.
<ErrorBoundary
fallbackRender={ErrorFallback}
onReset={() => {
// reset 관련 side effect 처리
}}
resetKeys={[userId]} // userId가 바뀌면 자동으로 에러 상태 초기화
>
<UserProfile userId={userId} />
</ErrorBoundary>
3. resetErrorBoundary()를 통한 수동 리셋
fallback UI 내부에서 제공되는 resetErrorBoundary()를 호출하면 에러 상태가 초기화되며 컴포넌트가 다시 렌더링된다.
4. useErrorBoundary() 훅
비동기 흐름에서 명령형으로 에러를 트리거해야 할 때 사용할 수 있다.
const { showBoundary } = useErrorBoundary();
try {
await fetchData();
} catch (e) {
showBoundary(e); // 렌더링 흐름과 무관하게 ErrorBoundary로 전달
}
에러를 감싸지 못하는 setTimeout, useEffect, async/await 같은 경우에서 유용하게 활용할 수 있다. 특히 비동기 코드에서 유용한데 컴포넌트의 에러 바운더리로는 오류를 잡아내지 못할 수 있기 때문이다.
쿼리 에러는 렌더링 타이밍에 발생해야 ErrorBoundary가 잡을 수 있다. React에서 ErrorBoundary는 렌더링 도중 발생한 동기 오류만 감지한다. 그런데 TanStack Query의 useQuery는 내부적으로 fetch 요청을 보내고, 에러가 발생하면 “에러 상태”로 기록된다.
단순히 ErrorBoundary만으로는 재시도가 어렵고 React Query의 에러 상태를 초기화하려면 QueryErrorResetBoundary가 필요하다. QueryErrorResetBoundary는 React Query에 “에러 상태를 리셋하라”는 명령을 전달할 수 있는 경계 컴포넌트이다.
<QueryErrorResetBoundary>
{({ reset }) => (
<ErrorBoundary onReset={reset} FallbackComponent={ErrorFallback}>
<MyComponent />
</ErrorBoundary>
)}
</QueryErrorResetBoundary>
MyComponent 안에서 useQuery()를 사용 → 에러 발생 시 → throw error → ErrorBoundary가 잡고 fallback UI 표시→ 사용자가 “다시 시도” → resetErrorBoundary() 호출 → onReset={reset} 연결됨 → React Query 에러 상태 초기화-> 컴포넌트 리렌더링 → 쿼리 재실행
즉, "로딩/성공/에러/재시도" 전체 흐름을 선언형 방식으로 구성할 수 있게 된다.
| 항목 | <QueryErrorResetBoundary> | useQueryErrorResetBoundary() |
|---|---|---|
| 구조 | 컴포넌트 방식 | 훅 방식 |
| 적용 범위 | 해당 컴포넌트 경계 내부 | 가장 가까운 경계 또는 전역 |
| 코드 가독성 | 리셋 범위가 명확하게 드러남 | 코드가 더 간결함 |
| 중첩 지원 | 가능 (여러 개 쓸 수 있음) | 가장 가까운 경계를 참조함 |
| 적합한 상황 | 페이지 또는 영역마다 리셋 처리 다르게 하고 싶을 때 | 전역 또는 간단한 에러 핸들링 처리 시 |
reset()은 react-error-boundary의 onReset prop에 전달되어야 쿼리 에러가 리셋된다.useQueryErrorResetBoundary
const { reset } = useQueryErrorResetBoundary()reset()은 React Query의 에러 상태를 초기화해서 다시 쿼리가 작동하도록 만든다.ErrorBoundary의 onReset에 연결하면 → 사용자가 “Try again”을 누를 때 쿼리를 다시 시도할 수 있게 된다.<QueryErrorResetBoundary> ← 쿼리 에러를 reset할 수 있는 경계
{({ reset }) => (
<AppErrorBoundary onReset={reset}> ← 실제 fallback UI를 띄우는 ErrorBoundary
<InstanceTableContent /> ← 내부에서 useInstanceData() 호출 (쿼리 실행)
</AppErrorBoundary>
)}
</QueryErrorResetBoundary>
<QueryErrorResetBoundary>와 <AppErrorBoundary>를 이렇게 중첩해서 사용하는 이유는 React Query의 쿼리 에러를 안전하게 감싸고 리셋할 수 있는 범위를 명확히 지정하기 위함이다.
useTableData() 호출 중 에러 발생 (throwOnError: true가 설정되어 있음)render 중 발생하므로 AppErrorBoundary가 이를 감지AppErrorBoundary는 fallback UI (에러 메시지 + "Try again" 버튼) 렌더링resetErrorBoundary()가 호출됨onReset={reset}을 통해 QueryErrorResetBoundary의 reset() 함수와 연결됨ErrorBoundary는 렌더링 과정에서 동기적으로 발생한 에러만 감지할 수 있다. 명시적인 처리가 필요해서 선언형 흐름과 살짝 어긋난다.QueryErrorResetBoundary와 ErrorBoundary는 reset을 수동으로 연결해야 한다. reset을 안 연결하거나, resetKeys를 제대로 설정하지 않으면 쿼리가 재시도되지 않는다.구현 실수가 나기 쉬운 구조이기 때문에 리팩토링 시 누락되기 쉽다. 선언형처럼 보이지만, 내부는 여전히 명령형 로직이 숨어 있는 것이다.ErrorBoundary는 단순히 "에러가 났다"는 것만 감지한다. 하지만 실제 애플리케이션에서는 에러를 더 세분화해서 다루고 싶을 수 있다. 인증 실패 (401), 권한 없음 (403), 서버 오류 (500), 네트워크 오류 (ERR_NETWORK), 클라이언트 로직 오류 등등. 이런 경우 fallback 컴포넌트 안에서 에러 객체를 직접 분기해야 하는데, 구조나 상태가 예상과 다르면 처리하기 어려울 수 있다.QueryErrorResetBoundary와 ErrorBoundary를 중첩해 사용할 때, 컴포넌트 계층 구조가 깊어지고 가독성이 떨어질 수 있다.ErrorBoundary로 잡을 수 있는 에러"렌더링 과정" 중 발생한 동기 에러
| 구분 | 예시 | 설명 |
|---|---|---|
| 렌더링 중 에러 | throw new Error() | JSX 반환 과정에서 오류 |
| 함수 컴포넌트 내부 오류 | 상태 기반 분기 오류 등 | render에서 직접 발생 |
useQuery의 에러 + throw error | if (error) throw error | ErrorBoundary가 catch 가능 |
| 자식 컴포넌트에서 발생한 오류 | <Child /> 안에서 오류 | 부모 ErrorBoundary가 감싸고 있으면 OK |
| constructor / getDerivedStateFromError | (클래스 컴포넌트 경우) | 해당 생명주기 함수에서 발생 시 catch 가능 |
"비동기/이벤트 핸들러/사이드 이펙트" 안의 에러
| 구분 | 예시 | 설명 |
|---|---|---|
setTimeout, Promise.catch 안의 에러 | setTimeout(() => { throw new Error(...) }) | React와 무관한 JS 실행 컨텍스트 |
useEffect 내부 에러 | useEffect(() => { throw new Error(...) }) | 사이드 이펙트 → ErrorBoundary가 감지 못함 |
| 이벤트 핸들러에서 발생한 에러 | onClick={() => throw new Error(...)} | React 이벤트 핸들러 안 → ErrorBoundary 미적용 |
| async/await 내부 에러 | await fetch() 중 발생한 에러 | JS 런타임 레벨에서 처리 필요 (try/catch로 처리해야 함) |
https://tanstack.com/query/latest/docs/framework/react/reference/QueryErrorResetBoundary
https://www.datoybi.com/error-handling-with-react-query/
https://developer-haru.tistory.com/86