본문 바로가기
Frontend2026년 4월 22일10분 읽기

TanStack Query v6 + RSC 통합 — HydrationBoundary·streamedQuery 실전

YS
김영삼
조회 1
TanStack Query v6 + RSC 통합 — HydrationBoundary·streamedQuery 실전

핵심 요약

TanStack Query v6는 React Server Components와의 통합이 1순위 변경. 서버에서 prefetch한 데이터를 클라이언트가 그대로 이어받는 패턴이 표준화됐다. 또한 streaming 응답을 query로 다루는 streamedQuery API가 추가됐다.

  • v6 출시: 2026-02
  • 핵심 API: HydrationBoundary, useSuspenseQuery, streamedQuery
  • 호환: React 19, Next.js 15+, Remix v3

1. QueryClient — 요청별 인스턴스

// app/providers.tsx
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'

export function Providers({ children }: { children: React.ReactNode }) {
  const [client] = useState(() => new QueryClient({
    defaultOptions: {
      queries: {
        staleTime: 60 * 1000,
        gcTime: 5 * 60 * 1000,
      },
    },
  }))
  return <QueryClientProvider client={client}>{children}</QueryClientProvider>
}

모듈 레벨 singleton 절대 금지. SSR에서 다른 사용자 데이터 섞임.

2. 서버 prefetch + dehydrate

// app/posts/page.tsx (RSC)
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { Posts } from './Posts'

export default async function PostsPage() {
  const queryClient = new QueryClient()

  await queryClient.prefetchQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('https://api/posts').then(r => r.json()),
  })

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <Posts />
    </HydrationBoundary>
  )
}

3. 클라이언트 useSuspenseQuery

// app/posts/Posts.tsx
'use client'
import { useSuspenseQuery } from '@tanstack/react-query'
import { Suspense } from 'react'

function PostsList() {
  const { data } = useSuspenseQuery({
    queryKey: ['posts'],
    queryFn: () => fetch('/api/posts').then(r => r.json()),
  })
  return <ul>{data.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}

export function Posts() {
  return (
    <Suspense fallback={<Loading />}>
      <PostsList />
    </Suspense>
  )
}

4. v6 신기능 — streamedQuery

SSE·streaming 응답을 query처럼 다룸. 부분 업데이트.

import { streamedQuery } from '@tanstack/react-query'

const { data } = useQuery({
  queryKey: ['ai-response', prompt],
  queryFn: streamedQuery({
    queryFn: async ({ signal }) => {
      const res = await fetch('/api/llm', {
        method: 'POST',
        body: JSON.stringify({ prompt }),
        signal,
      })
      return res.body  // ReadableStream
    },
    refetchMode: 'reset',  // 또는 'append'
  }),
})
// data가 토큰별로 부분 업데이트됨

LLM·SSE 응답을 query 캐시 시스템 안에서 자연스럽게 다루는 핵심 기능.

5. Mutation 패턴

function CreatePost() {
  const queryClient = useQueryClient()
  
  const mutation = useMutation({
    mutationFn: (newPost) => fetch('/api/posts', {
      method: 'POST',
      body: JSON.stringify(newPost),
    }).then(r => r.json()),
    
    // optimistic update
    onMutate: async (newPost) => {
      await queryClient.cancelQueries({ queryKey: ['posts'] })
      const previous = queryClient.getQueryData(['posts'])
      queryClient.setQueryData(['posts'], (old) => [...old, { ...newPost, id: Date.now() }])
      return { previous }
    },
    onError: (err, newPost, context) => {
      queryClient.setQueryData(['posts'], context.previous)
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] })
    },
  })
}

6. Infinite Query — 무한 스크롤

const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam = 0 }) => fetch(`/api/posts?cursor=${pageParam}`).then(r => r.json()),
  getNextPageParam: (last) => last.nextCursor,
  initialPageParam: 0,
})

7. 흔한 함정 5가지

1) staleTime 너무 짧음

기본 0이면 hydrate 직후 즉시 stale 판정 → 클라이언트가 재 fetch. staleTime: 60s 이상 권장.

2) queryFn URL이 환경별로 달라야 함

const baseURL = typeof window === 'undefined'
  ? process.env.INTERNAL_API_URL
  : ''
// 서버에선 절대 URL 필요

3) Suspense 경계 누락

useSuspenseQuery는 반드시 Suspense 안에서.

4) hydration mismatch

Date·Math.random은 클라이언트에서만.

5) QueryClient singleton 오염

useState로 요청별 생성 — 모듈 레벨 절대 금지.

8. 도구

import { ReactQueryDevtools } from '@tanstack/react-query-devtools'

<QueryClientProvider client={client}>
  {children}
  {process.env.NODE_ENV === 'development' && <ReactQueryDevtools />}
</QueryClientProvider>

devtools에서 Server/Client 캐시 상태 비교가 가장 빠른 디버깅.

9. 큰 그림 — TanStack Query 정말 필요한가?

RSC가 충분히 강력해 단순 fetch는 RSC만으로 끝. TanStack Query 도입 정당화 조건:

  • infinite scroll·optimistic update 빈번
  • 복잡한 캐시 무효화 정책
  • WebSocket·SSE 실시간 데이터와 query 통합

단순 CRUD는 RSC + Server Actions로 충분.

자주 묻는 질문

v5 → v6 마이그레이션?codemod 제공. 90% 자동. 주요 변경은 streamedQuery 추가 + RSC 통합 정리.

SWR과 차이?

TanStack Query가 기능 풍부 (mutation·infinite·optimistic). SWR은 더 가벼움. RSC 통합은 둘 다 비슷.

서버 컴포넌트만으로 충분한 경우?fetch 한 번 + 정적 표시면 RSC만으로 OK. 클라이언트 측 mutation·실시간 동기화 필요하면 TanStack Query.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록