정의
서버 상태 캐싱 라이브러리(TanStack Query 등)에서 "내가 방금 쓴 like·댓글이 화면에 바로 보이게" 하고 싶을 때, 흔히 staleTime을 낮추려 한다. 하지만 그건 잘못된 노브다. staleTime은 데이터를 몇 초간 신선하다고 믿고 백그라운드 refetch를 참을지를 정하고, invalidateQueries는 그 staleTime을 무시하고 관련 쿼리를 즉시 다시 가져온다. 즉시 반영의 메커니즘은 staleTime이 아니라 invalidate다.
여기서 stale은 "낡았다고 표시된" 상태, refetch는 서버에서 데이터를 다시 가져오는 것, invalidate는 캐시를 "낡음"으로 찍어 다시 가져오게 만드는 동작을 뜻한다.
왜 필요한가
증상 시나리오: 사용자가 좋아요를 눌렀는데 화면이 바로 안 바뀐다. "그럼 staleTime을 0으로 낮추면 되겠지" 하면, 이번엔 창 포커스·컴포넌트 마운트마다 refetch가 터져 서버 트래픽이 폭증한다. 신선도와 트래픽이 서로 충돌하는 것처럼 보인다.
정답은 두 노브의 역할을 분리하는 것이다. staleTime은 신선도 SLA(예: "10초 안엔 반영")로 유지해 백그라운드 트래픽을 억제하고, 쓰기 뮤테이션이 성공한 뒤 관련 쿼리만 invalidate해서 그 순간에만 강제 refetch한다. 그러면 평소엔 트래픽이 조용하고, 내 쓰기 직후엔 즉시 최신화된다.
동작 원리
| 노브 | 무엇을 통제하나 | 즉시 반영에 쓰나 |
|---|---|---|
staleTime | 백그라운드/포커스/마운트 refetch 여부 | 아니오 |
invalidateQueries | 지금 즉시 refetch(활성) + 낡음 표시(비활성) | 예 |
- 활성(마운트된) 쿼리를 invalidate하면 staleTime과 무관하게 즉시 refetch한다.
- 비활성 쿼리는 낡음으로만 표시돼 다음 방문 때 refetch한다.
- invalidate에 넘기는 queryKey 접두어로 영향 범위를 좁힌다(스코프 무효화) — 관련 없는 쿼리까지 refetch하지 않게.
실무 적용
체감 즉시성이 중요하면 onMutate 낙관적 패치로 먼저 바꾸고, onSettled에서 invalidate로 서버 진실과 맞춘다.
트레이드오프
- staleTime을 낮춰 즉시성 확보: 구현은 간단하지만 모든 refetch 지점에서 트래픽이 는다. 규모가 커지면 서버가 앓는다.
- staleTime 유지 + 쓰기 후 invalidate: 코드가 조금 늘지만 트래픽과 즉시성을 동시에 만족. 스코프 무효화로 refetch 범위까지 통제된다.
- 다운로드 바이트까지 아끼려면 조건부 요청(ETag/304)과 병행한다 — invalidate가 refetch를 유발해도 내용이 안 바뀌었으면 304로 본문을 안 받는다.
사용하면 안 되는 경우
- 실시간 협업처럼 초 단위로 남의 변경이 쏟아지는 화면 — invalidate 폴링보다 구독(WebSocket/Realtime)이 맞다.
- 쓰기 즉시성이 필요 없는 순수 읽기 대시보드 — staleTime만으로 충분하면 invalidate를 남발하지 않는다.
흔한 실수
- 즉시 반영이 안 된다고 staleTime을 0으로 낮춰 트래픽 폭증.
- invalidate 스코프가 너무 넓어(
invalidateQueries()전체) 무관한 쿼리까지 다 refetch. - 낙관적 패치 없이 invalidate만 써서 쓰기→refetch 사이 깜빡임이 보임.
- onError 롤백을 빼먹어 실패한 낙관적 상태가 화면에 남음.
관련 개념
- ssr-prefetch-query-cache-hydration — 쿼리 캐시를 서버에서 채워 넘기는 하이드레이션
- optimistic-outbox-rebase — 오프라인·낙관적 쓰기를 큐로 관리하고 수렴
- race-safe-async-ui-requests — 낙관적 롤백과 요청 레이스 처리