병렬 & 의존 쿼리: 여러 쿼리를 조합하기

실제 앱에서는 여러 API를 동시에 호출하거나, 한 API의 결과를 바탕으로 다른 API를 호출해야 하는 경우가 많습니다.

병렬 쿼리 (Parallel Queries)

여러 useQuery를 나란히 쓰면 기본적으로 병렬 실행됩니다.

function Dashboard() {
  const usersQuery = useQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const postsQuery = useQuery({ queryKey: ['posts'], queryFn: fetchPosts });
  const statsQuery = useQuery({ queryKey: ['stats'], queryFn: fetchStats });
 
  // 세 API 요청이 동시에 실행됨
  if (usersQuery.isLoading || postsQuery.isLoading || statsQuery.isLoading) {
    return <Spinner />;
  }
 
  return (
    <div>
      <UserList users={usersQuery.data} />
      <PostList posts={postsQuery.data} />
      <StatsPanel stats={statsQuery.data} />
    </div>
  );
}

useQueries: 동적 개수의 병렬 쿼리

쿼리 개수가 렌더링 시점에 결정된다면 useQueries를 사용합니다. 훅은 조건부로 호출할 수 없기 때문입니다.

import { useQueries } from '@tanstack/react-query';
 
function UserProfiles({ userIds }) {
  // userIds 배열 길이에 따라 동적으로 쿼리 생성
  const queries = useQueries({
    queries: userIds.map(id => ({
      queryKey: ['user', id],
      queryFn: () => fetchUser(id),
    })),
  });
 
  // queries: [{ data, isLoading, ... }, { data, isLoading, ... }, ...]
  const isLoading = queries.some(q => q.isLoading);
  const users = queries.map(q => q.data).filter(Boolean);
 
  if (isLoading) return <Spinner />;
  return (
    <div>
      {users.map(user => <UserCard key={user.id} user={user} />)}
    </div>
  );
}
// combine 옵션: 여러 쿼리 결과를 하나로 합치기
const { users, isLoading } = useQueries({
  queries: userIds.map(id => ({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  })),
  combine: (results) => ({
    users: results.map(r => r.data).filter(Boolean),
    isLoading: results.some(r => r.isLoading),
  }),
});

의존 쿼리 (Dependent Queries)

첫 번째 쿼리 결과가 있어야 두 번째 쿼리를 실행할 수 있는 경우, enabled를 활용합니다.

function UserPosts({ userId }) {
  // 1단계: 사용자 정보 가져오기
  const { data: user } = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  });
 
  // 2단계: 사용자의 팀 ID가 있을 때만 팀 정보 가져오기
  const { data: team } = useQuery({
    queryKey: ['team', user?.teamId],
    queryFn: () => fetchTeam(user.teamId),
    enabled: !!user?.teamId, // user.teamId가 있을 때만 실행
  });
 
  return (
    <div>
      <p>사용자: {user?.name}</p>
      <p>팀: {team?.name ?? '팀 없음'}</p>
    </div>
  );
}

enabled: !!user?.teamId로 인해:

  • user가 아직 로딩 중이면 → 두 번째 쿼리 실행 안 됨
  • user는 있지만 teamId가 없으면 → 두 번째 쿼리 실행 안 됨
  • user.teamId가 있으면 → 두 번째 쿼리 실행

의존 쿼리 체인

여러 단계로 이어지는 경우도 같은 방식으로 처리합니다.

function OrderDetails({ orderId }) {
  // 1단계: 주문 정보
  const { data: order } = useQuery({
    queryKey: ['order', orderId],
    queryFn: () => fetchOrder(orderId),
  });
 
  // 2단계: 주문의 사용자 정보 (order가 있을 때만)
  const { data: user } = useQuery({
    queryKey: ['user', order?.userId],
    queryFn: () => fetchUser(order.userId),
    enabled: !!order?.userId,
  });
 
  // 3단계: 사용자의 배송지 (user가 있을 때만)
  const { data: address } = useQuery({
    queryKey: ['address', user?.addressId],
    queryFn: () => fetchAddress(user.addressId),
    enabled: !!user?.addressId,
  });
 
  return <div>...</div>;
}

모든 쿼리가 완료된 후 처리

function TeamDashboard({ teamId }) {
  const membersQuery = useQuery({
    queryKey: ['team', teamId, 'members'],
    queryFn: () => fetchTeamMembers(teamId),
  });
 
  const projectsQuery = useQuery({
    queryKey: ['team', teamId, 'projects'],
    queryFn: () => fetchTeamProjects(teamId),
  });
 
  const allLoaded = !membersQuery.isLoading && !projectsQuery.isLoading;
  const hasError = membersQuery.isError || projectsQuery.isError;
 
  if (!allLoaded) return <Spinner />;
  if (hasError) return <ErrorMessage />;
 
  return (
    <div>
      <MemberList members={membersQuery.data} />
      <ProjectList projects={projectsQuery.data} />
    </div>
  );
}

Suspense 모드와 병렬 쿼리

suspense: true 옵션을 사용하면 모든 쿼리가 완료될 때까지 Suspense fallback을 보여줍니다.

function Dashboard() {
  // 각 useQuery에 suspense 옵션 추가
  const { data: users } = useSuspenseQuery({ queryKey: ['users'], queryFn: fetchUsers });
  const { data: posts } = useSuspenseQuery({ queryKey: ['posts'], queryFn: fetchPosts });
 
  // 로딩 체크 불필요 - Suspense가 처리
  return (
    <div>
      <UserList users={users} />
      <PostList posts={posts} />
    </div>
  );
}
 
// 부모에서 Suspense로 감쌈
<Suspense fallback={<Spinner />}>
  <Dashboard />
</Suspense>

정리

패턴방법사용 시점
병렬 쿼리여러 useQuery 나란히독립적인 데이터를 동시에 가져올 때
동적 병렬useQueries쿼리 개수가 동적일 때
의존 쿼리enabled 조건이전 쿼리 결과가 필요할 때