Skill
2026년 01월 04일

next.js app router 에서의 tanstack-query (4)

Lesson 4: useSuspenseQuery와 React Suspense (선언형 프로그래밍의 정점)

1. 기획 배경 (Scenario)

지금까지 우리는 데이터를 가져올 때 로딩 상태를 이렇게 처리했습니다.

export function ProductList(){ const {data, isLoading, isError} = useQuery(...) if (isLoading) return <div>로딩 중...</div>; if (isError) return <div>에러 발생!</div>; return <div>data...</div>; }

이 방식은 직관적이지만 몇 가지 아쉬움이 있다.

1. 명령형(Imperative): 컴포넌트 안에서 "상태에 따라 무엇을 할지" 일일이 if문으로 지시

2. 워터폴(Waterfall): 부모 컴포넌트가 로딩 중이면 자식 컴포넌트는 렌더링조차 시작되지 않음

3. UI 찢어짐(Tearing): 여러 컴포넌트가 각자 로딩 스피너를 돌리면 화면이 산만

Lesson 4의 목표: React의 Suspense 기능을 활용하여, 데이터 로딩과 에러 처리를 컴포넌트 밖으로 위임(Delegate)하는 것

해당 Lesson 에서의 핵심 개념

  • useSuspenseQuery 의 사용
  • Suspense, ErrorBoundary로 로딩과 에러를 감싸 처리

useSuspenseQuery

  • 기존 useQuery를 사용하는것에서 Suspense 기능을 활용한 쿼리를 날릴라면 useSuspenseQuery를 사용한다

Suspense

  • 데이터가 아직 준비가 안된 컴포넌트는 부모에 의해 잠시 멈추고(Suspense), 준비가 될때까지 부모에의한 Fallback을 보여준다.
  • 즉 컴포넌트는 ‘데이터가 항상 있다’ 는 전제하에 코드를 할 수 있게 됨.

코드 적용

// app/suspense/page.tsx import { ProductListSkeleton, SuspenseProductList } from "@/entities/product"; import { Suspense } from "react"; export default function SuspensePage() { return ( <div className="mx-auto max-w-2xl p-10"> <h1 className="mb-6 text-3xl font-bold text-purple-700"> 🔮 Suspense & Streaming </h1> <p className="mb-8 text-gray-500"> 이 페이지는 useSuspenseQuery를 사용합니다. <br /> 데이터가 로딩되는 동안, 컴포넌트 내부가 아닌{" "} <strong>부모의 Suspense Fallbac</strong>이 보여집니다. </p> <Suspense fallback={<ProductListSkeleton />}> <SuspenseProductList /> </Suspense> </div> ); }
// .../suspense-product-list.tsx "use client"; import { fetchProductsFromAPI } from "@/shared/api/products/products-api"; import { useSuspenseQuery } from "@tanstack/react-query"; export function SuspenseProductList() { // isLoading이 없음 // 데이터가 아직 없으면 여기서 컴포넌트 실행이 일시정지(suspensd) 됨. const { data } = useSuspenseQuery({ queryKey: ["products", "suspense"], queryFn: fetchProductsFromAPI, }); return ( <div className="grid gap-4"> {/* 💡 data가 무조건 있다고 보장되므로 옵셔널 체이닝(?.)이 필요 없습니다. */} {data.map((product) => ( <div key={product.id} className="rounded-lg border-2 border-purple-100 bg-purple-50 p-4 shadow-sm" > <div className="text-lg font-bold text-purple-900"> {product.name} </div> <div className="text-purple-600"> {product.price.toLocaleString()} </div> </div> ))} </div> ); }

🚨 주의사항 및 해결책 (Trap & Fix)

* Server Action 제약: client 컴포넌트에서 useSuspenseQuery 내의 Server Action을 직접 호출하면 렌더링 도중

서버 함수를 호출하려 한다는 에러가 발생할 수 있습니다.

* 해결책:

1. 서버에서 미리 데이터를 가져오는 Prefetching(Lesson 3)을 결합합니다.

2. 또는 렌더링 단계에서 안전하게 호출 가능한 API Route(fetch) 방식을 사용합니다.

3.useSuspenseQuery대신 서버컴포넌트를 사용한다.

💡👨‍🏫 선생님의 최종 정리: "useSuspenseQuery는 개발자가 '로딩'이라는 상태에서 벗어나 '비즈니스 로직'에만 집중하게 해줍니다. 이제 화면의 주인공은 로딩 바가 아니라 데이터입니다!"