---
title: "요청을 쪼갰는데 화면은 여전히 가장 느린 요청을 기다린다"
description: "요청을 섹션별로 나눴는데도 합산 로딩 플래그 때문에 화면은 50초 뒤에야 바뀌었습니다. 섹션 단위 pending 판정과 TanStack Query v5 의 pending 함정을 정리했습니다."
date: 2026-10-16
updated: 2026-10-16T08:36:48.375Z
tags: [react, tanstack-query, frontend, loading-state]
canonical: https://blog.wooncloud.com/posts/combined-loading-flag-waits-for-slowest-request
---

통합검색 결과 화면을 만지다가 이상한 걸 발견했습니다. 결과가 섹션 셋(내 파일, 공유, 프로젝트)으로 나뉘는데, 서버가 세 갈래를 모두 끝낸 뒤 한 번에 응답하다 보니 가장 느린 갈래가 전부를 붙잡고 있었습니다.

그래서 클라이언트에서 섹션별로 요청을 3개로 쪼갰습니다. 그런데 요청 로그를 보면 두 섹션은 0.6초에 끝났는데, 화면은 세 번째 섹션이 끝나는 50초 뒤까지 가운데 스피너 하나뿐이었습니다.

## 요청은 나눴는데 왜 화면은 그대로일까

화면의 로딩 판정을 여전히 하나로 합쳐 두었기 때문입니다.

```ts
isLoading: anyPending && !allLoaded   // "셋 다 올 때까지 로딩"
```

섹션별 대기 표시 코드는 이미 있었습니다. 하지만 이 전체 로딩 분기가 먼저 걸리는 바람에 한 번도 렌더되지 않았죠. **요청 분할의 효과가 화면에서는 0이었던 셈입니다.**

![요청을 3개로 나눠도 합산 플래그로 판정하면 가장 느린 50초 요청 동안 스피너만 보이고, 섹션별로 판정하면 먼저 온 두 섹션을 0.6초에 바로 그릴 수 있다.](/images/posts/combined-loading-flag-waits-for-slowest-request/933a25cf-848f-4edb-87cb-7cd50e34d403.svg)

## 같은 자리에 함정이 두 개 더 있었다

첫 번째는 실패가 "도착"을 영원히 막는다는 점입니다. `allLoaded` 를 "세 쿼리 모두 data 가 있음"으로 두면 한 쿼리가 에러로 끝날 때 영영 false 가 됩니다.

여기에 "도착 전이면 data 를 undefined 로" 같은 파생값을 두면 더 곤란해집니다. 에러가 나는 순간 화면이 "마지막으로 성공한 스냅샷"으로 떨어지는데, 그 스냅샷은 fetching 도중에 찍힌 것이라 늦게 도착한 섹션의 결과도, 실패한 섹션의 오류 표시도 없습니다. 느린 요청은 결국 타임아웃으로 끝나기 쉬워서 이게 오히려 주 경로가 됩니다.

두 번째는 꺼진 쿼리도 pending 이라는 점입니다. TanStack Query v5 에서 `enabled: false` 쿼리는 data 가 없으니 `status === 'pending'`, 즉 `isPending: true` 입니다. "pending 이면 스켈레톤"으로 읽으면 검색어 없이 들어온 화면에서 스켈레톤이 영영 남습니다.

![꺼진 쿼리를 pending 으로만 읽으면 스켈레톤이 영영 남지만, enabled 상태까지 같이 보면 빈 결과로 바로 그려진다.](/images/posts/combined-loading-flag-waits-for-slowest-request/33d6f438-4cd3-4152-910b-9795624fb07e.webp)

## 섹션 단위로 판정하기

섹션마다 `pending = isPending && data === undefined` 이면 그 섹션만 제목과 스켈레톤으로 그리고, 도착한 섹션은 바로 그립니다. "결과 없음"은 대기 중인 섹션이 하나도 없을 때만 띄웁니다.

도착 여부로 data 를 undefined 로 만들지도 않습니다. 섹션별로 살아 있는 값(빈 배열, 결과, error 플래그)을 그대로 합쳐 쓰고, 전체 오류 화면은 모든 섹션이 실패했을 때만 띄웁니다.

pending 은 "켜져 있는데 아직 없음"으로 읽어야 합니다. `enabled && isPending` 으로 보거나 호출부에서 입력값 존재로 게이트하면 됩니다. 참고로 `isLoading` 은 `isPending && isFetching` 이라 이 문제가 없습니다.

```ts
const qs = useQueries({ queries: panes.map((p) => ({ queryKey: [p, q], queryFn, enabled: !!q })) });

const sections = qs.map((r) => ({
  data: r.data,
  error: r.isError,
  pending: !!q && r.isPending,          // 꺼진 쿼리는 대기로 치지 않는다
}));
const anyPending = sections.some((s) => s.pending);
const isEmpty = !anyPending && sections.every((s) => !s.error && !s.data?.length);
const allFailed = sections.every((s) => s.error);
// 렌더: allFailed → 전체 오류 / isEmpty → 결과 없음 / 그 외 → 섹션마다 pending ? 스켈레톤 : 결과|섹션오류
```

요청을 쪼개는 건 절반이고, 화면의 판정까지 같이 쪼개야 체감이 달라진다는 걸 다시 확인했습니다. 메모 겸 남겨 둡니다.

참고

- [TanStack Query, Queries (status, fetchStatus)](https://tanstack.com/query/latest/docs/framework/react/guides/queries)
- [TanStack Query, Disabling Queries](https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries)
- [TanStack Query, useQueries](https://tanstack.com/query/latest/docs/framework/react/reference/useQueries)
