🌐 웹개발

Suspense

React Suspense

비동기 렌더링을 선언적으로 처리. 로딩 상태 관리 간소화.

📖 상세 설명

React Suspense는 컴포넌트의 렌더링을 데이터가 준비될 때까지 "일시 중단"하고 대체 UI(fallback)를 표시하는 선언적 로딩 처리 메커니즘입니다. if (loading) 조건문 없이도 깔끔하게 비동기 상태를 관리할 수 있습니다.

Suspense는 원래 React.lazy와 함께 코드 스플리팅용으로 도입되었지만, React 18부터 데이터 페칭에도 사용할 수 있게 확장되었습니다. 서버 컴포넌트의 async 함수나 TanStack Query의 suspense 옵션과 결합하여 동작합니다.

여러 Suspense boundary를 중첩하여 사용하면 세분화된 로딩 상태를 관리할 수 있습니다. 부모 Suspense는 자식들이 모두 준비될 때까지 기다리고, 각 자식 Suspense는 독립적으로 로딩 상태를 처리합니다.

Streaming SSR에서 Suspense는 핵심 역할을 합니다. 서버에서 Suspense boundary까지 렌더링한 후 fallback을 포함한 HTML을 먼저 전송하고, 데이터가 준비되면 나머지를 스트리밍으로 보내 점진적으로 채워넣습니다.

💻 코드 예제

import { Suspense, lazy } from 'react';

// 1. 코드 스플리팅과 Suspense
const HeavyChart = lazy(() => import('./HeavyChart'));
const AdminPanel = lazy(() => import('./AdminPanel'));

function Dashboard() {
  return (
    <div>
      <h1>대시보드</h1>

      <Suspense fallback={<ChartSkeleton />}>
        <HeavyChart />
      </Suspense>

      <Suspense fallback={<div>관리자 패널 로딩중...</div>}>
        <AdminPanel />
      </Suspense>
    </div>
  );
}

// 2. TanStack Query와 Suspense
import { useSuspenseQuery } from '@tanstack/react-query';

function UserProfile({ userId }: { userId: string }) {
  // suspense: true 옵션이 자동 활성화
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  // data는 항상 존재 (undefined 체크 불필요)
  return <div>{user.name}</div>;
}

function ProfilePage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

// 3. 중첩 Suspense로 세분화된 로딩
function ProductPage({ id }: { id: string }) {
  return (
    <Suspense fallback={<PageSkeleton />}>
      <ProductInfo id={id} />

      <div className="grid grid-cols-2 gap-4">
        <Suspense fallback={<ReviewsSkeleton />}>
          <ProductReviews productId={id} />
        </Suspense>

        <Suspense fallback={<RelatedSkeleton />}>
          <RelatedProducts productId={id} />
        </Suspense>
      </div>
    </Suspense>
  );
}

// 4. ErrorBoundary와 함께 사용
import { ErrorBoundary } from 'react-error-boundary';

function SafeDataFetch() {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<Loading />}>
        <DataComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

// 5. SuspenseList로 로딩 순서 제어 (실험적)
import { SuspenseList } from 'react';

function Feed() {
  return (
    <SuspenseList revealOrder="forwards" tail="collapsed">
      <Suspense fallback={<PostSkeleton />}>
        <Post id="1" />
      </Suspense>
      <Suspense fallback={<PostSkeleton />}>
        <Post id="2" />
      </Suspense>
    </SuspenseList>
  );
}

🗣️ 실무 대화 예시

기술 미팅에서:

"로딩 스피너가 너무 많아서 정신없어요. Suspense boundary를 적절히 묶으면 하나의 스켈레톤으로 깔끔하게 처리할 수 있어요. UX팀이랑 로딩 단위 협의해보죠."

기술 면접에서:

"Suspense의 동작 원리는요?" - "자식 컴포넌트가 Promise를 throw하면 Suspense가 catch해서 fallback을 렌더링합니다. Promise가 resolve되면 다시 자식을 렌더링합니다."

코드 리뷰에서:

"useSuspenseQuery 쓰면 if (isLoading) 체크 안 해도 돼요. 대신 반드시 Suspense로 감싸야 하고, ErrorBoundary도 같이 넣어서 에러 처리하세요."

⚠️ 주의사항

🔗 관련 용어

Streaming SSR React Code Splitting TanStack Query Error Boundary

📚 더 배우기

📄 React 공식 문서 - Suspense 📄 TanStack Query - Suspense Guide 📄 React - lazy()