← 홈으로 돌아가기

🔄 React Query + Suspense 통합

선언적 데이터 페칭과 로딩 상태 관리

📦 패키지 설치 필요

npm install @tanstack/react-query

현재는 임시 로딩 상태로 구현되어 있습니다. 패키지 설치 후 코드 주석을 해제하세요.

✨ React Query + Suspense의 핵심 특징

React Query

  • • 자동 캐싱 및 백그라운드 재검증
  • • 중복 요청 제거 (deduplication)
  • • Stale/Fresh 상태 자동 관리
  • • DevTools로 캐시 시각화

Suspense 통합

  • • 선언적 로딩 상태 처리
  • • 컴포넌트 수준 경계 설정
  • • 병렬 데이터 로딩 최적화
  • • 에러 경계와 함께 사용

사용자 데이터

Posts

1️⃣ QueryClient 설정

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 60 * 5, // 5분 동안 fresh 상태 유지
      gcTime: 1000 * 60 * 10,   // 10분 동안 캐시 보관
      retry: 3,                  // 실패 시 3번 재시도
      refetchOnWindowFocus: true, // 창 포커스 시 재검증
    },
  },
});

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

2️⃣ useSuspenseQuery 사용

import { useSuspenseQuery } from '@tanstack/react-query';

// API 함수
async function fetchUser(userId: number) {
  const response = await fetch(`/api/users/${userId}`);
  if (!response.ok) throw new Error('Failed to fetch user');
  return response.json();
}

// 컴포넌트
function UserProfile({ userId }: { userId: number }) {
  // ✅ Suspense를 트리거 (로딩 중일 때)
  // ✅ 캐시가 있으면 즉시 반환
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],  // 캐시 키
    queryFn: () => fetchUser(userId),
  });

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

// Suspense로 감싸기
function App() {
  return (
    <Suspense fallback={<LoadingSkeleton />}>
      <UserProfile userId={1} />
    </Suspense>
  );
}

3️⃣ 병렬 데이터 로딩

function UserDashboard({ userId }: { userId: number }) {
  // 🚀 두 쿼리가 병렬로 실행됨
  const { data: user } = useSuspenseQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });

  const { data: posts } = useSuspenseQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
  });

  return (
    <div>
      <UserInfo user={user} />
      <PostList posts={posts} />
    </div>
  );
}

// ✅ Suspense는 모든 쿼리가 완료될 때까지 대기
<Suspense fallback={<LoadingSpinner />}>
  <UserDashboard userId={1} />
</Suspense>

4️⃣ 중첩 Suspense (점진적 로딩)

function UserPage({ userId }: { userId: number }) {
  return (
    <div>
      {/* 1. 사용자 정보 먼저 로딩 */}
      <Suspense fallback={<UserSkeleton />}>
        <UserProfile userId={userId} />
      </Suspense>

      {/* 2. 포스트는 독립적으로 로딩 */}
      <Suspense fallback={<PostsSkeleton />}>
        <UserPosts userId={userId} />
      </Suspense>

      {/* 3. 댓글은 가장 나중에 로딩 */}
      <Suspense fallback={<CommentsSkeleton />}>
        <RecentComments userId={userId} />
      </Suspense>
    </div>
  );
}

// ✅ 각 섹션이 독립적으로 로딩되어 더 빠른 초기 렌더링
// ✅ 사용자는 먼저 로딩된 부분부터 볼 수 있음

5️⃣ 에러 처리 (Error Boundary)

import { ErrorBoundary } from 'react-error-boundary';

function ErrorFallback({ error, resetErrorBoundary }) {
  return (
    <div className="bg-red-50 border-2 border-red-200 rounded-lg p-6">
      <h2 className="text-red-900 font-bold mb-2">
        데이터 로딩 실패
      </h2>
      <p className="text-red-700 mb-4">{error.message}</p>
      <button
        onClick={resetErrorBoundary}
        className="px-4 py-2 bg-red-600 text-white rounded"
      >
        다시 시도
      </button>
    </div>
  );
}

function App() {
  return (
    <ErrorBoundary FallbackComponent={ErrorFallback}>
      <Suspense fallback={<LoadingSpinner />}>
        <UserDashboard userId={1} />
      </Suspense>
    </ErrorBoundary>
  );
}

// ✅ API 에러는 ErrorBoundary가 캐치
// ✅ 로딩 상태는 Suspense가 처리
// ✅ 완벽하게 분리된 관심사

✅ 장점

  • 선언적 로딩 상태: if-else 분기 제거
  • 자동 캐싱: 중복 요청 방지
  • 백그라운드 재검증: 항상 최신 데이터
  • 병렬 로딩 최적화: Waterfall 방지
  • DevTools 제공: 캐시 상태 시각화
  • TypeScript 지원: 완벽한 타입 추론

⚠️ 주의사항

  • SSR 주의: Hydration 이슈 가능
  • 에러 경계 필수: ErrorBoundary 설정
  • 캐시 전략 이해: staleTime, gcTime 조정
  • 번들 크기: ~40KB 추가
  • 러닝 커브: 캐싱 개념 학습 필요

💡 캐싱 전략 이해하기

staleTime (Fresh → Stale 전환 시간)

데이터가 "최신"으로 간주되는 시간. 이 시간 내에는 재요청하지 않음.

staleTime: 1000 * 60 * 5 // 5분

gcTime (Garbage Collection Time)

사용하지 않는 캐시가 메모리에서 제거되기까지 대기 시간.

gcTime: 1000 * 60 * 10 // 10분

refetchOnWindowFocus (창 포커스 시 재검증)

사용자가 다른 탭에서 돌아올 때 자동으로 데이터 갱신.

refetchOnWindowFocus: true

🎯 실전 사용 패턴

1. 자주 변경되는 데이터 (예: 실시간 알림)

staleTime: 0, refetchInterval: 5000

2. 거의 변경되지 않는 데이터 (예: 사용자 프로필)

staleTime: Infinity, gcTime: 1000 * 60 * 30

3. 중간 정도 (예: 게시글 목록)

staleTime: 1000 * 60 * 5, refetchOnWindowFocus: true