요청을 쪼갰는데 화면은 여전히 가장 느린 요청을 기다린다
요청을 섹션별로 나눴는데도 합산 로딩 플래그 때문에 화면은 50초 뒤에야 바뀌었습니다. 섹션 단위 pending 판정과 TanStack Query v5 의 pending 함정을 정리했습니다.
통합검색 결과 화면을 만지다가 이상한 걸 발견했습니다. 결과가 섹션 셋(내 파일, 공유, 프로젝트)으로 나뉘는데, 서버가 세 갈래를 모두 끝낸 뒤 한 번에 응답하다 보니 가장 느린 갈래가 전부를 붙잡고 있었습니다.
그래서 클라이언트에서 섹션별로 요청을 3개로 쪼갰습니다. 그런데 요청 로그를 보면 두 섹션은 0.6초에 끝났는데, 화면은 세 번째 섹션이 끝나는 50초 뒤까지 가운데 스피너 하나뿐이었습니다.
요청은 나눴는데 왜 화면은 그대로일까
화면의 로딩 판정을 여전히 하나로 합쳐 두었기 때문입니다.
isLoading: anyPending && !allLoaded // "셋 다 올 때까지 로딩"섹션별 대기 표시 코드는 이미 있었습니다. 하지만 이 전체 로딩 분기가 먼저 걸리는 바람에 한 번도 렌더되지 않았죠. 요청 분할의 효과가 화면에서는 0이었던 셈입니다.
같은 자리에 함정이 두 개 더 있었다
첫 번째는 실패가 "도착"을 영원히 막는다는 점입니다. allLoaded 를 "세 쿼리 모두 data 가 있음"으로 두면 한 쿼리가 에러로 끝날 때 영영 false 가 됩니다.
여기에 "도착 전이면 data 를 undefined 로" 같은 파생값을 두면 더 곤란해집니다. 에러가 나는 순간 화면이 "마지막으로 성공한 스냅샷"으로 떨어지는데, 그 스냅샷은 fetching 도중에 찍힌 것이라 늦게 도착한 섹션의 결과도, 실패한 섹션의 오류 표시도 없습니다. 느린 요청은 결국 타임아웃으로 끝나기 쉬워서 이게 오히려 주 경로가 됩니다.
두 번째는 꺼진 쿼리도 pending 이라는 점입니다. TanStack Query v5 에서 enabled: false 쿼리는 data 가 없으니 status === 'pending', 즉 isPending: true 입니다. "pending 이면 스켈레톤"으로 읽으면 검색어 없이 들어온 화면에서 스켈레톤이 영영 남습니다.
섹션 단위로 판정하기
섹션마다 pending = isPending && data === undefined 이면 그 섹션만 제목과 스켈레톤으로 그리고, 도착한 섹션은 바로 그립니다. "결과 없음"은 대기 중인 섹션이 하나도 없을 때만 띄웁니다.
도착 여부로 data 를 undefined 로 만들지도 않습니다. 섹션별로 살아 있는 값(빈 배열, 결과, error 플래그)을 그대로 합쳐 쓰고, 전체 오류 화면은 모든 섹션이 실패했을 때만 띄웁니다.
pending 은 "켜져 있는데 아직 없음"으로 읽어야 합니다. enabled && isPending 으로 보거나 호출부에서 입력값 존재로 게이트하면 됩니다. 참고로 isLoading 은 isPending && isFetching 이라 이 문제가 없습니다.
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 ? 스켈레톤 : 결과|섹션오류요청을 쪼개는 건 절반이고, 화면의 판정까지 같이 쪼개야 체감이 달라진다는 걸 다시 확인했습니다. 메모 겸 남겨 둡니다.
참고