구독을 알면 보이는 손잡이들
queryKey 가 의존성 배열이고 staleTime 과 gcTime 이 다른 질문인 이유를 풀어요.
지난 편 에서 useQuery 가 캐시를 구독한다는 걸 봤어요. 저는 한동안 staleTime 이랑 gcTime 을 그냥 외워서 썼는데, 구독 모델을 머리에 넣고 나니 그제서야 왜 두 개로 나뉘어 있는지 보이더라구요. queryKey, staleTime, gcTime, select 가 전부 "구독을 어떻게 다룰까" 라는 한 질문의 답이라는 게 이 편의 축이에요.
queryKey 는 의존성 배열처럼 움직여요
queryKey 는 useEffect 의 의존성 배열과 똑같이 동작해요. 키가 바뀌면 다른 쿼리로 인식돼서 자동으로 다시 받아요. 그래서 요청을 식별하는 변수는 전부 키에 넣어야 해요.
useQuery({
queryKey: ['todos', { status, page }],
queryFn: () => fetchTodos({ status, page }),
})useEffect 로 페칭을 직접 짜다 보면, 탭을 빨리 바꿨을 때 직전 응답이 늦게 도착해 화면을 덮어쓰는 경쟁 상태 를 손으로 막아야 했어요. 키 기반 캐시는 이 식별을 대신 해줘서, 같은 키면 캐시를 쓰고 키가 바뀌면 새로 받아요.
신선도와 보관 기간은 다른 손잡이예요
제일 많이 헷갈리는 한 쌍이 staleTime 과 gcTime 이에요. 둘은 다른 질문에 답해요. staleTime 은 "언제 다시 받을까" 예요. 기본값이 0 이라, 마운트하거나 창에 다시 포커스할 때마다 새로 받아요. 자주 안 바뀌는 데이터는 이 값을 늘려주면 쓸데없는 요청이 줄어요. gcTime 은 "안 쓰는 캐시를 언제 버릴까" 고요. 화면에서 사라져 구독자가 0 이 된 데이터를 메모리에 얼마나 더 둘지 정하는데, 기본이 5 분이에요.
"a garbage collection timeout is set using gcTime to delete and garbage collect the query (defaults to 5 minutes)." - TanStack Query
staleTime 은 신선도, gcTime 은 보관 기간이에요. "자꾸 요청이 나가요" 는 보통 staleTime 쪽, "뒤로 갔다 오니 캐시가 날아갔어요" 는 gcTime 쪽 이야기예요.
select 도 구독을 안다면 더 잘 쓰게 돼요. 쿼리 결과를 변형해서 그 조각만 구독하거든요. 고른 값이 그대로면 리렌더를 건너뛰어서, 큰 응답에서 일부만 보는 화면의 불필요한 리렌더를 줄여줘요.
const { data: count } = useQuery({
queryKey: ['todos'],
queryFn,
select: (todos) => todos.length,
})todos 배열 전체가 바뀌어도 길이가 같으면 이 컴포넌트는 안 움직여요. 구독하는 조각을 좁힌 거예요.
읽기와 쓰기가 만나는 자리
1편에서 useQuery 는 구독, getQueryData 는 일회성 읽기로 갈라놨는데, 둘이 같이 일하는 대표적인 자리가 낙관적 업데이트예요. 서버 응답을 기다리지 않고 화면을 먼저 바꿔치기하는 패턴이죠.
여기서 getQueryData 는 롤백용 스냅샷을 뜨는 데 쓰고, setQueryData 는 캐시를 직접 갈아끼우는 데 써요. 구독은 useQuery 가 하고, 캐시를 손으로 만지는 건 이 명령형 짝이 맡아요. 흐름은 이렇게 흘러가요.
onMutate에서 스냅샷
cancelQueries 로 진행 중 페치를 멈추고, getQueryData 로 지금 캐시를 떠둬요.
const prev = queryClient.getQueryData(['todos'])낙관적으로 먼저 반영
서버 응답을 기다리지 않고 setQueryData 로 캐시를 미리 바꿔요. 구독 중인 화면이 곧바로 갱신돼요.
queryClient.setQueryData(['todos'], add(newTodo))실패하면 롤백
onError 에서 떠둔 스냅샷으로 되돌려요. 사용자는 잠깐 보였다 사라지는 걸 봐요.
queryClient.setQueryData(['todos'], ctx.prev)끝나면 서버와 맞추기
onSettled 에서 invalidateQueries 로 진짜 서버 상태를 다시 받아 정합을 맞춰요.
queryClient.invalidateQueries({ queryKey: ['todos'] })setQueryData 로 캐시를 바꾸는 순간, 그 키를 구독하던 화면이 알아서 갱신돼요. 우리가 화면을 직접 건드리지 않아도요. 결국 읽기 쪽 구독과 쓰기 쪽 조작이 같은 캐시 위에서 맞물리는 거예요.
시리즈를 닫으며
getQueryData 로 값을 꺼내 그리지 않고 useQuery 를 쓰는 이유는 하나로 모여요. 화면은 서버 상태의 변화를 따라가야 하니까, 값을 읽는 게 아니라 변화를 구독하는 거예요. useQuery 로 화면을 그리고, 이벤트 핸들러나 롤백처럼 렌더 바깥에서 값이 필요할 때만 getQueryData 와 setQueryData 를 꺼내 쓰면 돼요. 에러 처리도 재시도도 캐시 수명도, 전부 이 구독 모델 위에 얹힌 손잡이고요.
같은 데이터 페칭이라도 SWR 은 이 그림을 조금 다르게 그려요. 두 라이브러리의 성격 차이가 궁금하면 SWR과 TanStack Query 가 다른 이유 에서 비교해뒀어요.
자주 묻는 질문
답을 펼치기 전에 스스로 답해보세요