무엇
streamedQuery는 스트림(AsyncIterable)에서 데이터 조각을 하나씩 받아 쿼리 데이터에 쌓아 주는 queryFn 헬퍼다. 조각이 도착할 때마다 데이터가 자라난다.
일반 쿼리는 결과가 한 번에 오지만, streamedQuery는 조각이 이어서 온다. LLM 토큰 스트림이나 서버가 나눠 보내는 응답처럼 점진적으로 도착하는 데이터에 쓴다. v5 시점에서 실험적(experimental) API라 이름 앞에 접두어가 붙는다.
import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query'
useQuery({
queryKey: ['chat'],
queryFn: streamedQuery({ streamFn: fetchChatChunks }),
})
무슨 일이 일어나나
streamFn은 AsyncIterable을 담은 Promise를 돌려준다. 쿼리는 첫 조각이 오기 전까지 pending이고, 첫 조각이 도착하면 success로 넘어가되 스트림이 끝날 때까지 fetchStatus는 fetching으로 남는다. 기본값으로 데이터는 받은 조각들의 배열로 쌓인다.
refetchMode가 다시 가져올 때의 동작을 정한다. 'reset'(기본)은 데이터를 비우고 다시 pending으로 돌아가고, 'append'는 기존 데이터에 이어 붙이며, 'replace'는 스트림이 끝난 뒤 한 번에 교체한다. reducer를 주면 배열 대신 원하는 형태로 조각을 합칠 수 있다(이때 initialValue가 필수).
사용법
streamFn으로 async generator를 넘긴다. yield한 조각들이 순서대로 데이터 배열에 쌓인다.
import { experimental_streamedQuery as streamedQuery } from '@tanstack/react-query'
async function* fetchChatChunks(): AsyncGenerator<string> {
const res = await fetch('/api/chat')
const reader = res.body!.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
yield decoder.decode(value) // 조각 하나
}
}
function Chat() {
const { data } = useQuery({
queryKey: ['chat'],
queryFn: streamedQuery({ streamFn: () => fetchChatChunks() }),
})
// data: string[] — 지금까지 도착한 조각들
return <p>{data?.join('')}</p>
}
실무 예시
LLM 응답처럼 토큰이 하나씩 오는 화면에서, 도착한 조각을 실시간으로 이어 붙여 보여 준다. refetchMode: 'reset'이면 재요청 시 화면이 비워지고 처음부터 다시 쌓인다.
const { data, fetchStatus } = useQuery({
queryKey: ['completion', prompt],
queryFn: streamedQuery({
streamFn: () => streamCompletion(prompt),
refetchMode: 'reset',
}),
})
// fetchStatus === 'fetching' 이면 아직 스트림이 흐르는 중
왜 중요한가
streamedQuery는 좁은 문제 하나 — 점진적으로 도착하는 데이터 — 를 위한 도구다. 대부분의 앱에는 필요 없지만, LLM 스트리밍이나 청크 응답을 다뤄야 하면 조각 누적·상태 전환을 직접 짜지 않아도 되게 해 준다. 실험적 단계라 널리 쓰이지는 않으며, 해당 요구가 있을 때만 꺼내 보면 되는 특수 목적 헬퍼다.
Reference