선언적 데이터 페칭과 로딩 상태 관리
npm install @tanstack/react-query
현재는 임시 로딩 상태로 구현되어 있습니다. 패키지 설치 후 코드 주석을 해제하세요.
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>
);
}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>
);
}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>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>
);
}
// ✅ 각 섹션이 독립적으로 로딩되어 더 빠른 초기 렌더링
// ✅ 사용자는 먼저 로딩된 부분부터 볼 수 있음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가 처리
// ✅ 완벽하게 분리된 관심사데이터가 "최신"으로 간주되는 시간. 이 시간 내에는 재요청하지 않음.
staleTime: 1000 * 60 * 5 // 5분사용하지 않는 캐시가 메모리에서 제거되기까지 대기 시간.
gcTime: 1000 * 60 * 10 // 10분사용자가 다른 탭에서 돌아올 때 자동으로 데이터 갱신.
refetchOnWindowFocus: truestaleTime: 0, refetchInterval: 5000staleTime: Infinity, gcTime: 1000 * 60 * 30staleTime: 1000 * 60 * 5, refetchOnWindowFocus: true