리액트(React)를 사용하여 데이터를 불러오는 컴포넌트를 만들 때, 우리는 보통 다음과 같은 코드를 작성하곤 합니다.
if (isLoading) return <LoadingSpinner />;
if (isError) return <ErrorMessage />;
return <DataView data={data} />;
익숙한 코드지만, 이 방식은 '명령형(Imperative)'에 가깝습니다. 컴포넌트 하나가 비즈니스 로직뿐만 아니라 로딩 처리, 에러 처리라는 세 가지 책임을 모두 떠안게 되기 때문입니다. 프로젝트가 커질수록 모든 컴포넌트에 이런 중복 코드가 들어가게 되고, UI의 일관성은 떨어집니다.
오늘은 리액트가 지향하는 '선언적 UI'의 완성형인 Suspense와 ErrorBoundary를 활용해, 비정상적인 상태(로딩, 에러)를 컴포넌트 밖으로 우아하게 밀어내는 전략을 알아보겠습니다.
1. 명령형 UI vs 선언적 UI
1.1 무엇이 문제인가?
명령형 방식에서는 개발자가 "로딩 중이면 스피너를 보여주고, 아니면 데이터를 보여줘"라고 일일이 로직을 지시해야 합니다. 이는 컴포넌트를 무겁게 만들고, 테스트를 어렵게 하며, 코드의 흐름을 방해합니다.
1.2 선언적 방식의 지향점
선언적 방식은 "이 컴포넌트는 데이터가 있을 때 이렇게 그려져야 해"라고 정의만 합니다. 데이터가 아직 준비되지 않았거나(Loading), 가져오는 데 실패했을 때(Error)의 처리는 컴포넌트를 감싸고 있는 '부모(상위 환경)'에게 맡깁니다.
2. Suspense: 데이터 로딩을 기다리는 우아한 방법
Suspense는 하위 컴포넌트가 무언가를 '기다리고 있다'는 것을 리액트에게 알리는 도구입니다.
2.1 주요 특징
- 관심사 분리: 컴포넌트 내부에서 isLoading을 체크할 필요가 없습니다.
- 로딩 UI의 중앙 집중화: 여러 개의 컴포넌트가 로딩 중일 때 하나의 스켈레톤 UI로 묶어서 보여줄 수 있습니다.
import { Suspense } from 'react';
function App() {
return (
<Layout>
<Suspense fallback={<SkeletonUI />}>
<UserProfile /> {/* 내부에서 데이터 패칭 중이라면 fallback이 보임 */}
</Suspense>
</Layout>
);
}
이렇게 작성하면 UserProfile은 데이터가 성공적으로 도착했을 때의 로직만 담으면 됩니다. 데이터가 비어있을 때 발생하는 런타임 에러 걱정에서도 자유로워집니다.
3. ErrorBoundary: 애플리케이션의 안전망
에러 바운더리는 하위 컴포넌트 트리 어디에서든 자바스크립트 에러가 발생했을 때, 앱 전체가 하얗게 변하는(White Screen) 것을 방지하고 대신 폴백(Fallback) UI를 보여주는 컴포넌트입니다.
3.1 왜 필요한가?
리액트에서 렌더링 중 에러가 발생하면 전체 컴포넌트 트리가 언마운트됩니다. 사용자 입장에서는 서비스가 갑자기 꺼지는 것과 같습니다. ErrorBoundary를 사용하면 특정 영역의 에러가 전체 앱으로 전파되는 것을 막을 수 있습니다.
3.2 선언적 에러 처리 예시
(참고: 현재 에러 바운더리는 클래스 컴포넌트로만 작성이 가능하거나, react-error-boundary 라이브러리를 사용합니다.)
import { ErrorBoundary } from 'react-error-boundary';
function ErrorFallback({ error, resetErrorBoundary }) {
return (
<div role="alert">
<p>문제가 발생했습니다.</p>
<pre>{error.message}</pre>
<button onClick={resetErrorBoundary}>다시 시도</button>
</div>
);
}
function App() {
return (
<ErrorBoundary FallbackComponent={ErrorFallback} onReset={() => {/* 상태 초기화 */}}>
<Suspense fallback={<Loading />}>
<DataDependentComponent />
</Suspense>
</ErrorBoundary>
);
}
4. 실무 패턴: 선언적 UI의 중첩 구조
실무에서는 보통 이 두 가지를 겹쳐서 사용합니다. 이를 통해 '로딩 -> 에러 -> 데이터 성공'이라는 데이터 패칭의 3단계를 완벽하게 제어할 수 있습니다.
- 가장 바깥쪽: ErrorBoundary (치명적인 에러 방어)
- 중간: Suspense (비동기 데이터 로딩 대기)
- 안쪽: 실제 비즈니스 로직 컴포넌트
이 구조를 사용하면 개발자는 안쪽 컴포넌트에서 데이터가 '무조건 존재한다'고 가정하고 코드를 짤 수 있습니다. 이를 'Suspense-enabled Data Fetching'이라고 부르며, React Query(TanStack Query)와 함께 쓸 때 가장 강력한 시너지를 냅니다.
5. 선언적 UI 도입의 장점
- 가독성 폭발: 컴포넌트 본연의 목적(UI 정의)에만 집중할 수 있어 코드가 짧아지고 명확해집니다.
- UX 개선: 로딩과 에러 화면을 디자이너가 의도한 대로 일관성 있게 보여줄 수 있습니다.
- 유지보수 용이: 로딩 스피너 디자인을 바꾸고 싶다면, 수백 개의 컴포넌트를 수정하는 대신 상위의 Suspense fallback만 고치면 됩니다.
'개발' 카테고리의 다른 글
| [TS] any 대신 unknown을 써야 하는 이유와 타입 가드 활용법 (0) | 2026.04.27 |
|---|---|
| [Architecture] 프론트엔드 클린 아키텍처: 도메인 로직과 UI 컴포넌트 분리하기 (0) | 2026.04.27 |
| [React] Props Drilling을 피하는 3가지 방법: Composition vs Context vs Zustand (0) | 2026.04.27 |
| [JS] 메인 스레드를 비워라! Web Worker를 활용한 무거운 연산 분산 처리 (0) | 2026.04.27 |
| [Web] 웹 폰트로 인한 레이아웃 시프트(CLS) 해결하기: font-display와 가상 폰트 (0) | 2026.04.27 |