React Suspense란?
React Suspense란?
React 앱에서 비동기 작업을 깔끔하게 처리하고 선언적으로 로딩 상태를 보여주는 React Suspense는 React 18 이후 중요한 기능으로 자리 잡았습니다. 이 글에서는 Suspense의 개념부터 실제 코드 예제까지 살펴보겠습니다.
Suspense란?
React Suspense는 컴포넌트가 비동기 작업(데이터 fetch, 코드 로딩 등)을 기다리는 동안 UI를 중단시키고, 기다리는 동안 Fallback UI를 보여주는 기능입니다. 이를 통해 React가 로딩 상태를 직접 관리하게 하여 코드를 더 선언적으로 관리할 수 있습니다
📌 Suspense의 핵심 아이디어
✨ Promise를 throw한다 — React가 이를 감지한다.
React는 컴포넌트 렌더링 중 Promise가 throw되면, 가장 가까운 <Suspense> boundary의 fallback을 보여주고, Promise가 resolve되면 다시 렌더링을 시도합니다. 이 방식 덕분에 Suspense는 비동기 로딩이라는 부수 효과를 컴포넌트 로직 밖으로 깔끔하게 분리할 수 있습니다.
⚙️ Suspense 동작 원리
다음 도식은 Suspense가 동작하는 흐름을 간단히 정리한 구조입니다:
컴포넌트 렌더링 시작 ↓ 비동기 데이터 접근 시도 ↓ Promise가 pending → React에게 throw ↓ React가 가장 가까운 Suspense로 전달 ↓ fallback UI로 전환 ↓ Promise resolve ↓ React가 다시 렌더링 시도 → 정상 UI 표시
위 흐름은 React가 내부적으로 Promise를 감지하고 retry하는 방식을 의미합니다.
🚀 실제 코드 예제
✅ 1) 코드 스플리팅 + Suspense
// LazyComponent.jsx export default function LazyComponent() { return <div>✨ 로드된 컴포넌트입니다!</div>; } // App.jsx import React, { Suspense, lazy } from "react"; const LazyComponent = lazy(() => import("./LazyComponent")); export default function App() { return ( <div> <h1>React Suspense 기본 예제</h1> <Suspense fallback={<div>🌀 컴포넌트 로딩 중...</div>}> <LazyComponent /> </Suspense> </div> ); }
👉 React.lazy()는 내부적으로 Promise를 기반으로 컴포넌트 모듈을 로드하며, Suspense는 이 Promise를 감지해 fallback을 보여줍니다.
✅ 2) Data Fetching + Suspense (Promise throw 기반)
// fetchUser.js function wrapPromise(promise) { let status = "pending"; let response; const suspender = promise.then( (res) => { status = "success"; response = res; }, (err) => { status = "error"; response = err; } ); return { read() { if (status === "pending") throw suspender; if (status === "error") throw response; return response; }, }; } export function fetchUser() { return wrapPromise( fetch("<https://jsonplaceholder.typicode.com/users/1>").then((r) => r.json() ) ); }
// App.jsx import React, { Suspense } from "react"; import { fetchUser } from "./fetchUser"; const userResource = fetchUser(); function UserProfile() { const user = userResource.read(); // Promise가 pending이면 throw return ( <div> <h2>👤 사용자 이름: {user.name}</h2> <p>📧 이메일: {user.email}</p> </div> ); } export default function App() { return ( <Suspense fallback={<div>📡 사용자 정보 로딩 중...</div>}> <UserProfile /> </Suspense> ); }
👉 Suspense는 이처럼 Promise를 throw하는 코드와 함께 써야 정상적으로 로딩 UI가 동작합니다.
✅ 3) React Query + Suspense (실무 활용)
import React, { Suspense } from "react"; import { useQuery, QueryClient, QueryClientProvider, } from "@tanstack/react-query"; const queryClient = new QueryClient({ defaultOptions: { queries: { suspense: true }, }, }); function UsersList() { const { data } = useQuery(["users"], () => fetch("<https://jsonplaceholder.typicode.com/users>").then((res) => res.json() ) ); return ( <ul> {data.map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } export default function App() { return ( <QueryClientProvider client={queryClient}> <Suspense fallback={<div>📦 사용자 목록 로딩...</div>}> <UsersList /> </Suspense> </QueryClientProvider> ); }
👉 React Query에서 suspense: true로 설정하면 Suspense가 로딩 처리에 관여합니다.
🤯 Suspense 내부 동작 구조 (소스 코드 기반 이해)
React Suspense는 그저 if (isLoading) showLoading() 같은 UI 트릭이 아닙니다. 내부적으로는 Fiber tree를 탐색하면서 Promise가 throw된 컴포넌트를 감지하고, 가장 가까운 Suspense boundary로 전달합니다. (jser.dev)
특히 React는:
- 렌더링 중 Promise가 throw되면 해당 컴포넌트의 렌더를 중단
- 가장 가까운 Suspense로 위치를 찾아가며 fallback UI 표시
- Promise resolve 시 React 스케줄러가 렌더 재시도
- 이전에 중단됐던 subtree를 다시 렌더
이런 흐름으로 Suspense는 동작한다고 이해할 수 있고, 이는 React Reconciler가 비동기 로직을 Fiber 구조와 연동해서 처리하는 결과입니다.
🧠 도식화: Suspense의 동작 흐름
┌───────────────────────────────┐ │ 1) 렌더링 중 Promise 발생 │ └───────────────┬───────────────┘ │ │ Promise throw return normal │ │ React가 감지 | ↓ ↓ 가장 가까운 Suspense 경계 탐색 ↓ ┌─────────────────────────┐ │ Suspense fallback 보여줌 │ └───────────┬──────────────┘ │ Promise resolve 기다림 ↓ React 스케줄러 → 재렌더링 시도 ↓ 정상 UI로 돌아오기
📌 팁 & 주의
✔ Suspense는 Promise throw 기반이어야 작동합니다 — useEffect 내부 fetch는 트리거하지 않음
✔ Suspense boundary를 적절히 분리하면 부분별 로딩 UI를 만들 수 있습니다
✔ Suspense는 SSR/Concurrent UI와 결합해 더 강력해집니다
🎯 마무리
React Suspense는 단순한 로딩 UI 추상화가 아니라, React 렌더러 자체가 Promise 기반 비동기 상태를 감지하고 처리하는 메커니즘입니다.
코드 스플리팅, 데이터 fetching, 라이브러리 연동 등 실전 상황에서 유용하게 쓰이며, 내부적으로는 Fiber tree와 Promise throw라는 독특한 패턴으로 구현되어 있습니다.