Afaik

Suspense

React의 Suspense는 어떻게 동작하나요?

React Suspense는 비동기 작업이 진행되는 동안 로딩 UI를 선언적으로 처리할 수 있게 해주는 컴포넌트입니다. 컴포넌트가 아직 준비되지 않았을 때 fallback UI를 보여줍니다.

Suspense의 핵심 개념

Suspense는 "일시 중단"이라는 의미로, 컴포넌트가 렌더링을 일시 중단하고 데이터를 기다리는 동안 로딩 상태를 보여주는 React의 기능입니다.

기본 동작 원리

Suspense는 Promise를 throw하는 컴포넌트를 감지하여 동작합니다:

// Suspense 기본 사용법
import { Suspense } from "react";

function App() {
  return (
    <Suspense fallback={<div>로딩 중...</div>}>
      <UserProfile />
      <PostList />
    </Suspense>
  );
}

// 데이터를 가져오는 컴포넌트
function UserProfile() {
  const user = useQuery(["user"], fetchUser); // 데이터가 없으면 Promise를 throw

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  );
}

Suspense와 함께 사용되는 패턴들

1. React.lazy를 통한 코드 스플리팅

import { Suspense, lazy } from "react";

// 동적 import를 통한 컴포넌트 레이지 로딩
const LazyComponent = lazy(() => import("./LazyComponent"));
const Dashboard = lazy(() => import("./Dashboard"));

function App() {
  return (
    <div>
      <Suspense fallback={<div>컴포넌트 로딩 중...</div>}>
        <LazyComponent />
      </Suspense>

      <Suspense fallback={<div>대시보드 로딩 중...</div>}>
        <Dashboard />
      </Suspense>
    </div>
  );
}

2. 데이터 페칭과 함께 사용

// React Query와 함께 사용하는 예시
import { Suspense } from "react";
import { useQuery } from "@tanstack/react-query";

function DataComponent() {
  const { data } = useQuery({
    queryKey: ["posts"],
    queryFn: fetchPosts,
    suspense: true, // Suspense 모드 활성화
  });

  return (
    <div>
      {data.map((post) => (
        <div key={post.id}>{post.title}</div>
      ))}
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>데이터 로딩 중...</div>}>
      <DataComponent />
    </Suspense>
  );
}

중첩된 Suspense 경계

여러 레벨의 Suspense를 사용하여 세밀한 로딩 제어가 가능합니다:

function App() {
  return (
    <div>
      <h1>My App</h1>

      {/* 전체 페이지 로딩 */}
      <Suspense fallback={<PageSkeleton />}>
        <Header />

        {/* 메인 콘텐츠 로딩 */}
        <Suspense fallback={<MainContentSkeleton />}>
          <MainContent />

          {/* 사이드바만 별도 로딩 */}
          <Suspense fallback={<SidebarSkeleton />}>
            <Sidebar />
          </Suspense>
        </Suspense>
      </Suspense>
    </div>
  );
}

Suspense 사용 시 주의사항

  1. 서버사이드 렌더링: SSR 환경에서는 React 18부터 지원됩니다.
  2. Error Boundary와 함께 사용: 데이터 페칭 실패 시를 대비해 Error Boundary를 함께 사용하세요.
  3. 무한 루프 방지: useEffect 내에서 Promise를 throw하면 무한 루프가 발생할 수 있습니다.

Error Boundary와 함께 사용하는 패턴

import { Suspense } from "react";
import { ErrorBoundary } from "react-error-boundary";

function ErrorFallback({ error }) {
  return (
    <div role="alert">
      <h2>문제가 발생했습니다</h2>
      <details>{error.message}</details>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<div>로딩 중...</div>}>
        <AsyncComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

Suspense의 동작 흐름

  1. 컴포넌트가 Promise를 throw: 비동기 작업이 필요한 컴포넌트에서 Promise를 던집니다.
  2. Suspense가 Promise를 감지: 가장 가까운 Suspense 경계에서 Promise를 잡습니다.
  3. Fallback UI 표시: 지정된 fallback 컴포넌트를 렌더링합니다.
  4. Promise 완료 대기: Promise가 완료될 때까지 기다립니다.
  5. 실제 컴포넌트 렌더링: Promise가 완료되면 원래 컴포넌트를 렌더링합니다.

실용적인 예시 - 사용자 대시보드

import { Suspense } from "react";

// 스켈레톤 컴포넌트들
const UserInfoSkeleton = () => (
  <div className="animate-pulse">
    <div className="mb-2 h-4 w-3/4 rounded bg-gray-200"></div>
    <div className="h-4 w-1/2 rounded bg-gray-200"></div>
  </div>
);

const PostsSkeleton = () => (
  <div className="space-y-4">
    {[...Array(3)].map((_, i) => (
      <div key={i} className="animate-pulse">
        <div className="mb-2 h-4 w-full rounded bg-gray-200"></div>
        <div className="h-4 w-2/3 rounded bg-gray-200"></div>
      </div>
    ))}
  </div>
);

function Dashboard() {
  return (
    <div className="dashboard">
      <h1>사용자 대시보드</h1>

      {/* 사용자 정보 섹션 */}
      <div className="user-section">
        <h2>프로필</h2>
        <Suspense fallback={<UserInfoSkeleton />}>
          <UserInfo />
        </Suspense>
      </div>

      {/* 게시물 섹션 */}
      <div className="posts-section">
        <h2>최근 게시물</h2>
        <Suspense fallback={<PostsSkeleton />}>
          <RecentPosts />
        </Suspense>
      </div>

      {/* 통계 섹션 - 독립적으로 로딩 */}
      <div className="stats-section">
        <h2>통계</h2>
        <Suspense fallback={<div>통계 로딩 중...</div>}>
          <UserStats />
        </Suspense>
      </div>
    </div>
  );
}

Concurrent Features와의 시너지

React 18의 Concurrent Features와 함께 사용하면:

  • startTransition: 상태 업데이트를 우선순위가 낮은 것으로 표시
  • useDeferredValue: 긴급하지 않은 상태 업데이트를 지연
  • Suspense: 이러한 기능들과 함께 더 부드러운 사용자 경험 제공
Edit on GitHub

Last updated on