리액트 스터디를 하면서 스터디원이 Suspense에 대해서 발표하였다. 내가 전혀 몰랐던 내용이라 흥미를 느꼈고, 프로젝트에서 사용하면 정말 유용할 거라는 생각이 들었다. 그래서 이번 기회로 Suspense를 다시 한번 제대로 공부하고 이해한 내용을 정리해보려고 한다.
기존 React의 비동기 처리 방식
보통 데이터를 가져올 때는 이렇게 작성한다.
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData().then(res => {
setData(res);
setLoading(false);
});
}, []);
if (loading) return <Spinner />;
예제 코드는 크게 문제가 없어 보이지만 실제 앱에서는 데이터가 하나만 있는 경우가 드물다. 사용자 정보, 게시글 목록, 댓글, 알림 등 여러 데이터를 동시에 가져와야 하는 경우가 많다.
실제 앱에서의 한계
데이터가 많아질수록 loading state가 많아지기 시작한다.
const [userLoading, setUserLoading] = useState(true);
const [feedLoading, setFeedLoading] = useState(true);
const [commentLoading, setCommentLoading] = useState(true);
그리고 조건문도 늘어난다.
return (
<>
{profileLoading
? <ProfileSkeleton />
: <Profile />}
{feedLoading
? <FeedSkeleton />
: <Feed />}
</>
)
컴포넌트가 많아질수록 관리 복잡도가 증가하는 등 문제가 생기기 시작했다.
처음에는 단순히 Spinner만 보여주면 되지 않을까 생각했다. 그런데 실제 서비스에서는 다르다.
페이지
├─ 프로필
├─ 게시글
└─ 추천 친구
예를 들어 이렇게 여러 영역이 있다. 만약 프로필은 이미 로딩 완료 했지만 게시글은 느리다면? 사용자는 프로필이라도 먼저 보고 싶을 것이다.
하지만 보통은 이렇게 전체 페이지를 막아버리는 경우가 많다.
if (loading) return <Spinner />;
즉, 페이지 전체가 하나의 로딩 상태에 묶여있는 상태가 된다. 그래서 부분별 로딩 처리가 필요해진다. 프로필을 먼저 보여주고 게시글만 skeleton으로 표시한다고 했을 때 기존 방식은 직접 관리해야 했다.
return (
<>
{profileLoading
? <ProfileSkeleton />
: <Profile />}
{feedLoading
? <FeedSkeleton />
: <Feed />}
</>
)
컴포넌트가 많아질수록 loading state가 증가하고, 조건문 증가, 관리 복잡도가 증가하는 문제가 생긴다.
useEffect fetch의 한계
또 하나의 문제는 fetch가 useEffect 이후에 실행된다는 점이었다.
useEffect(() => {
fetchUser().then(user => {
fetchPosts(user.id)
})
}, [])
user fetch 완료 대기, 이후 posts fetch 시작 이 순서로 실행된다. 즉, fetch가 연쇄적으로 이어지게 되는데 이를 waterfall이라고 한다.
waterfall이란?
하나의 작업이 끝나야 다음 작업이 시작되는 흐름을 의미한다.
Suspense는 무엇일까?
Suspense는 준비되지 않은 컴포넌트의 렌더링을 잠시 보류하는 메커니즘이다. 데이터를 fetch하는 기능도 아니고 spinner를 보여주는 기능도 아니다.
Suspnese를 이해하기 가장 쉬운 예시는 lazy였다.
import Page from "./Page";
이 경우 앱이 시작될 때 Page 컴포넌트 코드도 함께 다운로드된다. 그런데 여기서 문제가 발생한다. React가 <Page />를 렌더링 하려고 했는데 아직 JS 다운로드가 끝나지 않은 상태가 된다. 즉, React 입장에서는 렌더링 하려는 컴포넌트가 아직 준비되지 않은 상황이 된다.
lazy란?
const Page = lazy(() => import("./Page"));필요한 시점에 컴포넌트를 다운로드 하는 것이다.
여기서 Suspnse가 등장한다.
<Suspense fallback={<Loading />}>
<Page />
</Suspense>
동작 흐름은 다음과 같다.
- React가 <Page /> 렌더링 시도
- 아직 다운로드되지 않음
- 렌더링 잠시 보류
- fallback UI 표시
- 다운로드 완료 후 렌더링 재개
여기서 fallback은 기다리는 동안 보여주는 UI이다. 또, 이 영역을 Suspense Boundary라고 부른다.
Boundary 내부에서 준비되지 않은 컴포넌트가 발생하면 fallback UI로 대체된다. Suspense는 렌더링을 기다리는 경계(boundary)처럼 동작한다.
마무리
처음에는 Suspense를 단순히 loading UI를 보여주는 기능이라고 생각했다. 하지만 공부하면서 느낀 점은 React가 준비되지 않은 컴포넌트를 어떻게 기다릴지를 렌더링 흐름 안에서 관리한다는 점이었다.
기존에는 개발자가 loading state를 직접 만들고 조건문으로 UI를 분기했다면, Suspense는 React가 렌더링을 잠시 보류하고 fallback UI를 보여주는 방식으로 동작한다는 점이 인상적이었다.
'💻 STUDY > Frontend' 카테고리의 다른 글
| React 클라이언트 전역 상태 관리 (0) | 2026.06.07 |
|---|---|
| Storybook이란? — props가 많아질수록 개발이 느려진다고 느꼈다면 (0) | 2026.06.01 |
| useState의 snapshot과 batching (0) | 2026.05.17 |
| Pagination과 Infinite Scroll, React에서는 어떻게 구현할까? (0) | 2026.05.10 |
| Tailwind CSS가 가벼운 이유 (0) | 2026.05.05 |