[React] React.js 강좌 24. Suspense와 Error Boundary로 비동기 렌더링 다루기
React 18이 나오면서 가장 크게 달라진 개념 중 하나가 바로 **비동기 렌더링(Concurrent Rendering)**입니다.
이전까지는 데이터가 오기 전까지 화면이 멈추는 게 당연했는데,
이제는 React가 렌더링 중에도 다른 작업을 병렬로 처리하면서
더 부드럽고 자연스러운 사용자 경험을 만들 수 있게 됐죠.
그 중심에는 Suspense와 Error Boundary라는 두 가지 기능이 있습니다.
이 둘은 단순히 로딩 스피너를 보여주는 역할을 넘어서,
React 애플리케이션의 “안정성과 부드러움”을 책임지는 핵심 기술입니다.
Suspense, 그동안 몰랐던 진짜 역할
Suspense를 처음 봤을 때는 단순히
“데이터 로딩 중일 때 대체 화면을 보여주는 도구” 정도로만 생각했습니다.
하지만 실제로 써보면 그 이상의 존재라는 걸 알게 됩니다.
Suspense는 컴포넌트가 비동기 데이터가 준비될 때까지 기다리게 하는 기능입니다.
즉, 컴포넌트 렌더링 중 Promise가 던져지면
React는 렌더링을 잠시 멈추고 fallback(대체 UI)을 보여줍니다.
아래 코드를 보면 감이 잡힙니다.
import { Suspense } from 'react';
import UserProfile from './UserProfile';
function App() {
return (
<Suspense fallback={<div>로딩 중...</div>}>
<UserProfile />
</Suspense>
);
}
UserProfile이 데이터를 불러오는 동안
React는 자동으로 fallback을 렌더링합니다.
즉, “데이터가 완성될 때까지 기다린다”는 개념을
UI 레벨에서 처리할 수 있는 거죠.
이전에는 isLoading이라는 상태를 따로 관리해야 했는데,
이제는 React가 이걸 알아서 처리해줍니다.
그래서 코드가 깔끔해지고 유지보수가 훨씬 쉬워집니다.
Suspense를 React Query와 함께 쓰면
React Query도 Suspense를 지원합니다.
useQuery 훅에서 suspense: true 옵션을 주면,
데이터 로딩 상태를 직접 관리할 필요가 없습니다.
function UserProfile() {
const { data } = useQuery({
queryKey: ['user'],
queryFn: fetchUser,
suspense: true,
});
return <div>{data.name}님 반갑습니다!</div>;
}
이제 isLoading 체크를 하지 않아도 됩니다.
React Query가 내부적으로 Promise를 던지고,
Suspense가 이를 감지해서 fallback UI를 자동으로 보여줍니다.
이 구조의 장점은 로딩 상태를 일관되게 관리할 수 있다는 겁니다.
페이지마다 다른 로딩 스피너를 둘 필요 없이
최상위 Suspense로 한 번 감싸주면 끝입니다.
실무에서 이 방식을 적용했을 때
로딩 상태 관리 코드가 절반 가까이 줄어들었습니다.
특히 대시보드나 통계 페이지처럼 여러 API를 동시에 불러오는 경우,
Suspense가 없으면 로딩 조건문이 지옥처럼 늘어납니다.
Error Boundary, 실패도 안전하게 보여주는 방법
Suspense가 “기다리는 역할”이라면,
Error Boundary는 실패를 받아주는 역할입니다.
React는 컴포넌트 렌더링 중 에러가 발생하면
기본적으로 전체 트리를 중단시켜 버립니다.
하지만 Error Boundary를 쓰면
에러가 발생한 컴포넌트만 격리시켜
전체 애플리케이션이 멈추지 않도록 할 수 있습니다.
아래처럼 간단하게 작성할 수 있습니다.
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError() {
return { hasError: true };
}
componentDidCatch(error, info) {
console.error('에러 발생:', error, info);
}
render() {
if (this.state.hasError) {
return <div>문제가 발생했습니다. 다시 시도해주세요.</div>;
}
return this.props.children;
}
}
이제 에러가 발생해도
앱 전체가 흰 화면으로 멈추지 않습니다.
Suspense와 함께 쓰면
로딩, 성공, 실패의 흐름이 완벽하게 정리됩니다.
Suspense + ErrorBoundary = 완성된 사용자 흐름
이 두 기능을 함께 쓰면,
React 앱의 “상태 전환 흐름”이 훨씬 매끄럽습니다.
<ErrorBoundary>
<Suspense fallback={<div>데이터 불러오는 중...</div>}>
<UserList />
</Suspense>
</ErrorBoundary>
- 로딩 중 → Suspense가 fallback 렌더링
- 성공 시 → 정상 렌더링
- 실패 시 → ErrorBoundary가 대체 UI 렌더링
이 세 단계를 명확하게 구분하면,
사용자는 “멈췄다”는 느낌을 받지 않습니다.
무언가 로딩 중인지, 실패했는지가 명확하게 표현되니까요.
이 패턴은 특히 Next.js 13+ App Router 환경에서 매우 유용합니다.
loading.tsx, error.tsx 파일 구조가
Suspense와 Error Boundary 개념을 그대로 반영하고 있기 때문입니다.
실무에서 적용해본 결과
Suspense를 프로젝트에 처음 적용했을 때
가장 큰 변화는 코드의 가독성이었습니다.
isLoading, isError 상태를 매번 조건문으로 감싸지 않아도 되니까요.
UI 흐름이 단순해지고, 컴포넌트가 본래 역할에 집중할 수 있게 됩니다.
Error Boundary도 사용자 경험을 바꿔놓았습니다.
예전엔 API 하나가 실패하면 전체 화면이 깨졌는데,
이제는 “일부 데이터만 불러오지 못했습니다”라는 메시지로 대체됩니다.
이 차이는 사용자 입장에서 “불편함”과 “안정감”의 차이로 느껴집니다.
비동기 렌더링은 단순히 기술적인 개선이 아닙니다.
사용자에게 “이 서비스는 안정적이다”라는 신뢰를 주는 요소입니다.
React 18 이후의 환경에서 Suspense와 Error Boundary는
필수가 된 이유가 분명히 있습니다.
다음 글에서는 이 비동기 흐름 위에 전역 상태를 얹어보겠습니다.
Context API를 활용해 전역 데이터를 다루는 구조를
실제 예제 중심으로 풀어보겠습니다.
React 앱이 커질수록 왜 Context를 써야 하는지,
그 이유를 자연스럽게 느끼실 수 있을 겁니다.