mimizae 님의 블로그
Tanstack Query의 데이터 최신화 본문

웹 애플리케이션은 클라이언트와 서버의 상호작용으로 동작한다.
TanStack Query는 서버에서 데이터를 가져와 메모리에 캐싱하고, UI는 이 캐시된 데이터를 기반으로 화면을 그린다.
하지만!! 서버 데이터는 고정된 값이 아니다.
다른 사용자의 작업이나, 현재 사용자의 POST/PUT/DELETE 요청에 의해 서버 상태는 언제든지 변경될 수 있다.
이때 서버의 데이터가 변경되더라도 클라이언트에 캐싱된 데이터가 항상 즉시 갱신되는 것은 아니다.
따라서 클라이언트의 캐시를 서버의 최신 상태와 동기화하고, 그 결과를 UI에 반영하는 과정이 필요하다.
TanStack Query는 이러한 동기화 작업을 자동 또는 수동으로 처리할 수 있는 다양한 방법을 제공한다!!
데이터 자동 최신화
TanStack Query는 서버 상태가 바뀌었을 가능성이 있는 순간에 refetch를 자동으로 수행한다.
서버 데이터를 항상 실시간으로 알 수는 없기 때문에 바뀌었을 수 있는 타이밍을!! 트리거로 refetch 하는 전략을 사용한다.
대표적으로 다음의 경우에 자동으로 refetch 된다. 각각을 알아보자.
- 브라우저 윈도우 재포커싱 시 (refetchOnWindowFocus)
- 네트워크 재연결 시 (refetchOnReconnect)
- 특정 간격으로 폴링 시 (refetchInterval)
- 쿼리 상태가 stale이 되었을 시
1) refetchOnWindowFocus
사용자가 다른 탭을 보다가 해당 애플리케이션 탭으로 돌아왔을 때, 캐시된 데이터가 stale 상태라면 따로 새로고침을 하지 않았더라도 자동으로 데이터를 다시 가져오게 한다.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
staleTime: 0, // 기본값
})
만약 사용자 A가 todo 애플리케이션 탭에 머무르다가 다른 탭, 예를 들어 유튜브 탭으로 이동했다고 가정해보자!
이 시간 동안 사용자 A의 화면은 더 이상 todo 애플리케이션을 보고 있지 않기 때문에, 클라이언트에 캐싱된 데이터는 그대로 유지된 채 서버와의 통신은 일어나지 않는다.
그 사이에 다른 사용자 B가 해당 애플리케이션에 새로운 todo를 추가하면, 서버의 데이터는 변경된다.
하지만 사용자 A의 클라이언트는 이 변경 사실을 즉시 알 수 없기 때문에, 여전히 이전에 캐싱된 데이터를 기준으로 화면을 유지하고 있다.
이후 사용자 A가 다시 todo 애플리케이션 탭으로 돌아오면, 브라우저의 window focus 이벤트가 발생한다.
TanStack Query는 이 시점을 서버 데이터가 변경되었을 가능성이 있는 순간으로 판단한다.
그리고 그 판단과 동시에, 해당 query가 stale 상태이며 refetchOnWindowFocus 옵션이 활성화되어 있다면!! 자동으로 데이터를 refetch 한다.
그 결과 서버의 최신 todo 목록을 다시 받아오게 되고, 캐시가 갱신되면서 UI 또한 자동으로 업데이트된다.
이 덕분에 사용자 A는 페이지를 새로고침하지 않아도, 사용자 B가 추가한 새로운 todo를 자연스럽게 확인할 수 있는 것이다!
🤔 음? 근데 탭 전환이 focus 이벤트...??

❓TanStack Query에서 말하는 focus란 무엇일까
TanStack Query 문서를 보다 보면 refetchOnWindowFocus, focusManager 같은 용어가 자주 등장한다.
이를 처음 접하면 많은 사람들이 이 focus를 React의 input focus나 컴포넌트 focus 정도로 오해하곤 한다. (바로 나!!!;; 😅)
하지만 TanStack Query에서 말하는 focus는 그런 의미와는 전혀 다르다.
TanStack Query의 focus는 UI 요소의 포커스 상태가 아니라, 사용자가 현재 애플리케이션을 보고 있는가를 의미한다.
즉!! 데이터 동기화 관점에서의 focus다.
웹 환경에서는 브라우저 탭이 활성화되어 있는 상태를 의미하고, 모바일(React Native) 환경에서는 앱이 백그라운드가 아닌 포그라운드(active) 상태인지를 의미한다.
❓왜 focus 시점에 데이터를 다시 가져올까
사용자가 브라우저 탭을 다른 곳으로 옮겼다가 다시 돌아오거나, 모바일 앱을 잠시 백그라운드로 보냈다가 다시 실행하는 상황을 생각해보자.
이 시간 동안 서버에서는 다음과 같은 일이 충분히 일어날 수 있다.
- 다른 사용자가 데이터를 수정하거나 삭제했을 수 있고
- 백엔드 배치 작업이 실행되었을 수도 있으며
- 현재 사용자 계정의 상태가 변경되었을 수도 있다
하지만 클라이언트는 이 사실을 자동으로 알 방법이 없다.
그래서 TanStack Query는 이런 상황에서 다음과 같이 판단한다.
사용자가 다시 앱을 보고 있다면, 서버 데이터가 변경 되었을 가능성이 있으니 한 번 더 확인하자!
이 판단의 결과가 바로 focus 시 자동 refetch다.
2) refetchOnReconnect (오프라인 → 온라인 전환)
네트워크가 오프라인 상태에서 다시 온라인 상태로 전환되었을 때도, TanStack Query는 서버 데이터가 변경되었을 가능성이 있다고 판단한다.
사용자가 지하철을 타고 이동 중이거나, 와이파이가 불안정한 환경에서 잠시 네트워크 연결이 끊어졌다고 가정해보자!
이 동안 클라이언트는 서버와 통신할 수 없기 때문에, 캐시된 데이터는 그대로 유지된 채 화면에 표시된다.
그 사이 서버에서는 다른 사용자의 요청이나 백엔드 작업으로 인해 데이터가 변경되었을 수 있다.
이후 네트워크가 다시 연결되면, TanStack Query는 이를 감지하고 해당 query가 stale 상태라면 자동으로 refetch를 수행한다.
이 과정을 통해 사용자는 별도의 새로고침 없이도 네트워크 복구와 동시에 최신 서버 데이터를 확인할 수 있다.
3) refetchInterval
TanStack Query는 일정한 시간 간격으로 서버 데이터를 주기적으로 다시 가져오는 폴링(polling) 방식도 지원한다.
예를 들어 실시간 알림, 대시보드, 모니터링 화면처럼 서버 데이터가 자주 변경될 가능성이 있는 경우, 사용자 액션 없이도 데이터를 지속적으로 최신 상태로 유지할 필요가 있다.
이때 refetchInterval 옵션을 설정하면! 해당 query는 지정된 시간 간격마다 자동으로 refetch를 수행한다.
서버 데이터가 변경되었는지 여부와 관계없이 정기적으로 다시 요청을 보내기 때문에, 항상 비교적 최신에 가까운 데이터를 유지할 수 있다.
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 5000, // 5초마다 refetch
})
다만 불필요한 네트워크 요청이 늘어날 수 있기 때문에, 폴링은 필요한 경우에만 신중하게 사용하는 것이 좋다.
4) Query가 stale 상태가 되었을 때
위의 거의 모든 예시들에서 '해당 query가 stale 상태라면 자동으로 refetch를 수행'한다는 말이 포함 되어있는 것을 볼 수 있다.
TanStack Query에서 query가 stale 상태가 된다는 것은 이 데이터가 서버의 최신 상태와 다를 수도 있다고 판단되는 시점을 의미한다.
예를 들어 사용자 A가 todo 목록 화면에 처음 진입했다고 가정해 보자.
이때 서버로부터 todo 목록을 성공적으로 가져와 클라이언트 캐시에 저장한다.
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
기본 설정에서는 데이터를 한 번 가져온 직후, 해당 query는 곧바로 stale 상태가 된다.
즉, “지금 데이터는 화면에 쓰고 있지만, 언제든 서버와 달라졌을 가능성이 있다”라고 간주하는 것이다.
이 상태에서 사용자 A가 다른 탭으로 이동했다가 다시 돌아오거나, 네트워크가 끊겼다가 다시 연결되거나, 혹은 컴포넌트가 다시 마운트되면 TanStack Query는 stale 상태의 query를 대상으로 자동 refetch를 수행한다.
결과적으로 stale 상태는 즉시 데이터를 다시 가져온다는 의미가 아니라, 자동 리패치가 일어날 수 있는 준비 상태라고 이해하는 것이 정확하다.

아래의 코드 예제는 TanStack Query 공식 문서에서 제공하는 예제로, 서버 데이터가 여러 경로로 변경될 수 있는 상황에서
TanStack Query가 어떤 방식으로 캐시를 최신 상태로 유지하는지를 보여준다.
앞서 살펴본 자동 refetch 전략들이 실제 코드에서는 어떻게 활용되는지 자연스럽게 녹아 있는 것을 확인할 수 있다!!!
공식 문서의 예시 코드 전문
import React from 'react'
import {
QueryClient,
QueryClientProvider,
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
const queryClient = new QueryClient()
export default function App() {
return (
<QueryClientProvider client={queryClient}>
<Example />
</QueryClientProvider>
)
}
function Example() {
const queryClient = useQueryClient()
const [intervalMs, setIntervalMs] = React.useState(1000)
const [value, setValue] = React.useState('')
const { status, data, error, isFetching } = useQuery({
queryKey: ['todos'],
queryFn: async (): Promise<Array<string>> => {
const response = await fetch('/api/data')
return await response.json()
},
// Refetch the data every second
refetchInterval: intervalMs,
})
const addMutation = useMutation({
mutationFn: (add: string) => fetch(`/api/data?add=${add}`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
const clearMutation = useMutation({
mutationFn: () => fetch(`/api/data?clear=1`),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['todos'] }),
})
if (status === 'pending') return <h1>Loading...</h1>
if (status === 'error') return <span>Error: {error.message}</span>
return (
<div>
<h1>Auto Refetch with stale-time set to {intervalMs}ms</h1>
<p>
This example is best experienced on your own machine, where you can open
multiple tabs to the same localhost server and see your changes
propagate between the two.
</p>
<label>
Query Interval speed (ms):{' '}
<input
value={intervalMs}
onChange={(ev) => setIntervalMs(Number(ev.target.value))}
type="number"
step="100"
/>{' '}
<span
style={{
display: 'inline-block',
marginLeft: '.5rem',
width: 10,
height: 10,
background: isFetching ? 'green' : 'transparent',
transition: !isFetching ? 'all .3s ease' : 'none',
borderRadius: '100%',
transform: 'scale(2)',
}}
/>
</label>
<h2>Todo List</h2>
<form
onSubmit={(event) => {
event.preventDefault()
addMutation.mutate(value, {
onSuccess: () => {
setValue('')
},
})
}}
>
<input
placeholder="enter something"
value={value}
onChange={(ev) => setValue(ev.target.value)}
/>
</form>
<ul>
{data.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<div>
<button
onClick={() => {
clearMutation.mutate()
}}
>
Clear All
</button>
</div>
<ReactQueryDevtools initialIsOpen />
</div>
)
}
참고) 자동 refetch를 꺼야 하는 경우
TanStack Query 자동 refetch를 꺼야 하는 경우
TanStack Query는 기본적으로 서버 상태와 클라이언트 캐시를 최대한 자연스럽게 동기화하기 위해 다양한 자동 refetch 전략을 제공한다.
window focus, 네트워크 재연결, 컴포넌트 마운트, stale 상태 등 여러 이벤트를 기준으로 데이터를 다시 가져오도록 설계되어 있다.
하지만 모든 화면에서 이러한 자동 refetch가 항상 좋은 선택은 아니다!!
경우에 따라서는 오히려 불필요한 네트워크 요청을 유발하거나, 사용자 경험을 해칠 수도 있다.
이런 상황에서는 자동 refetch를 부분적으로 혹은 완전히 끄는 것이 더 적절하다.
1. 데이터가 거의 변하지 않는 경우
서버 데이터가 거의 변경되지 않는 화면에서는 자동 refetch가 큰 의미를 가지지 않는다.
예를 들어 서비스 소개 페이지, 약관, 공지사항처럼 하루에 한 번도 바뀌지 않을 수 있는 데이터라면 사용자가 탭을 이동할 때마다 데이터를 다시 가져오는 것은 불필요한 네트워크 비용만 증가시킨다.
이런 경우에는 staleTime을 길게 설정하거나 refetchOnWindowFocus를 비활성화하는 것이 좋다.
useQuery({
queryKey: ['terms'],
queryFn: fetchTerms,
staleTime: 1000 * 60 * 60 * 24, // 24시간
refetchOnWindowFocus: false,
})
2. 사용자 입력 중인 화면 (UX가 중요한 경우)
폼 입력 화면이나 에디터 화면처럼 사용자가 집중해서 작업 중인 상황에서는 자동 refetch가 오히려 문제가 될 수 있다.
예를 들어 게시글 작성 중인 화면에서 window focus 이벤트로 인해 query가 refetch되면서 컴포넌트가 리렌더링된다면, 입력 중이던 내용이 초기화되거나 사용자의 흐름이 끊길 수 있다.
이런 화면에서는 서버 데이터보다 사용자의 현재 입력 상태가 더 중요하다!!
따라서 자동 refetch를 꺼두고, 필요한 시점에만 수동으로 refetch하는 것이 안전하다.
useQuery({
queryKey: ['post'],
queryFn: fetchPost,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
3. 이미 polling이나 실시간 동기화를 사용 중인 경우
이미 refetchInterval을 사용해 주기적으로 데이터를 가져오고 있다면, focus나 reconnect 시점의 자동 refetch는 중복 요청이 될 수 있다.
특히 polling 주기가 짧은 경우에는 사용자가 탭을 이동할 때마다 refetch가 한 번 더 발생하면서 불필요한 서버 요청이 쌓일 수 있다. 이런 경우에는 polling만 유지하고 다른 자동 refetch 옵션은 꺼두는 것이 합리적이다!!
useQuery({
queryKey: ['notifications'],
queryFn: fetchNotifications,
refetchInterval: 3000,
refetchOnWindowFocus: false,
})
데이터 수동 최신화
TanStack Query가 스스로 판단해 refetch하는 것이 아니라, 개발자가 특정 시점에 캐시 동기화를 직접 트리거하는 것을 의미한다.
자동 최신화가 서버 데이터가 바뀌었을 가능성이 있는 순간을 기준으로 동작한다면,
수동 최신화는 서버 데이터가 바뀌었다는 사실을 개발자가 알고 있는 순간에 사용된다.
대표적인 예가 바로 mutation 이후다.
1. invalidQueries()
queryClient.invalidateQueries({ queryKey: ['todos'] })
invalidateQueries는 수동 최신화에서 가장 자주 사용되는 API다.
다만 이 메서드는 “자동인가? 수동인가?”를 가장 헷갈려하는 대상이기도 하다.
invalidateQueries는 정확히 무엇을 할까?
invalidateQueries는 즉시 데이터를 다시 가져오지 않는다. 대신!!!! 해당 query를 강제로 stale 상태로 만든다.
그 이후 실제 refetch 여부는 TanStack Query가 판단한다.
- 현재 화면에서 사용 중인 query라면 → 자동 refetch
- 사용 중이 아니라면 → 다음에 필요할 때 refetch
2. refetch()
const { refetch } = useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
})
<button onClick={() => refetch()}>
다시 불러오기
</button>
refetch는 말 그대로 개발자가 원할 때 즉시 서버 요청을 보내는 API다.
invalidateQueries와 달리, stale 여부를 따지지 않고 active 상태인지도 고려하지 않고 호출되는 순간 바로 refetch가 발생한다!!
이 방식은 다음과 같은 상황에서 적합하다.
- 사용자가 새로고침 버튼을 눌렀을 때
- 자동 최신화를 전부 꺼둔 화면
- 반드시 그 순간 최신 데이터가 필요할 때
다만 refetch는 조건 없이 요청을 보내기 때문에, 자주 호출하면 불필요한 네트워크 트래픽이 발생할 수 있다.
그래서 실무에서는 invalidateQueries보다 사용 빈도가 낮은 편이다.
3. setQueryData()
queryClient.setQueryData(['todos'], (oldTodos) => {
return [...oldTodos, newTodo]
})
setQueryData는 refetch를 발생시키지 않는다.
대신 클라이언트 캐시 자체를 직접 수정한다. 이 방식은 주로 다음과 같은 경우에 사용된다.
- optimistic update
- 서버 응답 구조를 정확히 알고 있는 경우
- 단순한 변경(추가, 삭제, 토글 등)
예를 들어 todo를 추가하는 mutation에서 서버 응답을 기다리지 않고 UI에 즉시 todo를 추가하고 싶다면 setQueryData가 매우 효과적이다.
다만 이 방식은 서버 상태를 클라이언트가 대신 가정하는 것이기 때문에 잘못 사용하면 서버와 클라이언트 상태가 어긋날 위험도 있다.
그래서 setQueryData는 가장 빠르지만, 가장 신중해야 하는 수동 최신화 방식이다.
참고) 과연... invalidateQueries는 자동인가 수동인가?
그래서... invalidateQueries는 자동일까, 수동일까??
정확히 말하면,
invalidateQueries는 '수동 트리거 + 자동 실행' 이다.
- 호출 자체는 개발자가 직접 한다 → 수동!
- refetch 시점과 여부는 TanStack Query가 결정한다 → 자동!
참고 자료 모음!
[Refactoring] tanstack-query로 데이터 최신화 하기
'FE' 카테고리의 다른 글
| CI/CD란 뭘까? (0) | 2025.12.26 |
|---|---|
| 페이지네이션에 대해서 (0) | 2025.12.21 |
| 낙관적 업데이트 적용하기 (0) | 2025.12.05 |
| 스크롤 감지 Tab-Bar 구현하기 (0) | 2025.11.26 |
| 실시간 알림 구현하기 (0) | 2025.11.16 |