React Query / SWR / Apollo는 재요청 중 이전 데이터를 어떻게 다룰까

  • CS
  • Apollo Client
  • React Query
  • SWR
2026-08-23

들어가며

Apollo Client는 쿼리 조건이 바뀌면 data가 잠깐 빈다. 이 빈 순간을 메워주는 게 previousData다. React Query나 SWR에도 비슷한 이름의 옵션이 있다. 세 라이브러리는 이 빈 순간을 같은 방식으로 메울까?

먼저 확인해야 할 건 이 빈 순간이 언제 찾아오느냐다. 세 라이브러리 모두 같은 조건으로 다시 조회하는 상황에서는 옵션 없이도 이전 값을 그대로 보여준다. 화면이 비는 건 조회 조건 자체가 바뀌는 순간, 즉 다른 자원으로 넘어가는 상황뿐이다.

같은 조건 재조회 이전 결과 1번 목록 그대로 화면 유지 값 그대로 보임 값 갱신 최신 결과 반영 같은 조건 재요청 응답 도착 다른 자원으로 전환 이전 결과 1번 글 상세 data: undefined 표시할 값 없음 값 채워짐 2번 글 표시 다른 자원으로 전환 응답 도착 이전 데이터 옵션이 메워야 하는 자리
같은 조건의 재조회는 화면이 한 번도 비지 않지만, 다른 자원으로 넘어갈 때만 data가 undefined인 단계(빨간색)가 끼어들고, 그 한 칸이 이전 데이터 옵션들이 메우려는 자리다.

React Query는 이전 값과 플래그를 함께 돌려준다

쿼리 키가 바뀌어 다른 자원으로 넘어가는 상황에서 React Query는 placeholderData에 이전 데이터를 그대로 반환하는 함수를 넘겨 이전 값을 유지한다. 이 방식이 자주 쓰이다 보니 라이브러리는 아예 keepPreviousData라는 헬퍼 함수로 만들어 제공한다.

import { useQuery, keepPreviousData } from '@tanstack/react-query'
 
function Todos({ page }: { page: number }) {
  const { data, isPlaceholderData } = useQuery({
    queryKey: ['todos', page],
    queryFn: () => fetchTodos(page),
    placeholderData: keepPreviousData,
  })
 
  return <TodoList todos={data?.todos ?? []} isStale={isPlaceholderData} />
}
  • placeholderData: keepPreviousData: 새 페이지 요청이 끝날 때까지 이전 페이지의 data를 그대로 화면에 남겨둠
  • isPlaceholderData: 지금 보이는 data가 새 페이지 응답이 아니라 이전 페이지에서 넘어온 값이라는 표시 (위 예시에서는 이 값을 isStale이라는 이름으로 TodoList에 그대로 전달해 목록을 흐리게 표시)

다만 새 페이지 요청이 실패하면 이 병합은 유지되지 않는다. placeholderData는 실제로 캐시된 값이 아니라 성공한 것처럼 보여주는 임시 데이터다. 요청이 실패하는 순간 data는 다시 undefined로, isPlaceholderData는 false로 돌아가면서 화면에는 이전 값 대신 빈 상태가 나타난다.

SWR도 이전 값과 로딩 상태를 함께 돌려준다

SWR은 기본 철학 자체가 stale-while-revalidate다. 같은 키를 다시 요청할 때는 캐시에 있는 값을 먼저 보여주면서 백그라운드에서 갱신하므로, 같은 키의 재요청만으로는 화면이 비지 않는다.

SWR도 키가 바뀌어 다른 자원으로 넘어가는 상황에서는 다르다. 이때를 위한 옵션이 SWR 2.0에서 추가된 keepPreviousData로, 새 키의 데이터가 도착하기 전까지 이전 키의 데이터를 유지한다. 공식 문서는 타이핑할 때마다 검색어가 바뀌는 실시간 검색 UI를 예로 든다. 검색어가 바뀔 때마다 결과가 사라졌다 나타나는 대신, 이전 검색 결과를 보여주면서 새 결과로 자연스럽게 바뀐다.

function Search() {
  const [keyword, setKeyword] = useState('')
 
  const { data, isLoading } = useSWR(`/search?q=${keyword}`, fetcher, {
    keepPreviousData: true,
  })
 
  return <SearchResults items={data?.items ?? []} isLoading={isLoading} />
}

SWR도 실패까지 가려주지는 않는다. 키가 바뀐 뒤 재요청이 실패해도 data는 이전 키의 값을 그대로 유지하고, 실패 정보는 error에 별도로 담긴다. 화면에 실패를 반영하려면 error를 직접 확인해야 한다.

Apollo는 병합과 판단을 개발자에게 맡긴다

위와 같이 React Query와 SWR은 이전 값을 data 자체에 담아 돌려준다. Apollo는 두 가지 지점에서 앞의 둘과 다르다. previousData를 data와 별도 필드로 주고, 어느 쪽을 보여줄지는 data ?? previousData처럼 직접 조합해야 한다. 그리고 지금 보이는 값이 이전 데이터인지 아닌지도 개발자가 직접 판단해야 한다.

React Query의 isPlaceholderData처럼 "지금 보이는 게 이전 값"이라고 알려주는 플래그가 Apollo에는 없다. 목록처럼 같은 종류의 자원을 다시 조회하는 화면이라면 이 조합이 자연스럽지만, 상세 페이지처럼 다른 항목으로 이동하는 화면에서 같은 방식을 그대로 쓰면 문제가 생긴다.

function TodoDetail({ id }: { id: string }) {
  const { data, previousData } = useQuery(TODO_QUERY, {
    variables: { id },
    fetchPolicy: 'cache-and-network',
  })
 
  const todo = data?.todo ?? previousData?.todo
  return <TodoDetailView todo={todo} />
}

A 항목을 보다가 B로 이동하면 previousData엔 A의 데이터가 남아 있다. B의 응답이 도착하기 전까지 화면엔 A의 제목과 내용이 그대로 떠 있다. 사용자는 이미 B로 이동했는데 화면엔 A의 정보가 보이는 셈이다. 목록에서는 자연스러웠던 병합이 상세 화면에서는 잘못된 정보를 보여주는 대가로 바뀐다.

조회 대상 id id: A id: B id 변경 id와 화면이 어긋나는 구간 시간 → 화면 (data ?? previousData) A의 todo 표시 A의 todo 표시(지연) B의 todo 표시 아직 B 응답 전 B 응답 도착
id는 이미 B인데 B 응답이 올 때까지 화면은 A의 todo를 붙들고 있어, 그 구간(빨간색)만큼 사용자가 보는 내용과 실제 위치가 어긋난다.

Apollo에는 그 구분이 없어서, 같은 종류의 재조회인지 다른 항목으로의 전환인지를 코드 작성자가 직접 판단해야 한다. 요청이 실패했을 때도 previousData는 그대로 남는다. 화면에 이전 값이 보이는 것과 별개로 error 상태는 직접 확인해야 실패를 놓치지 않는다.

React Query SWR Apollo data placeholderData 자동 병합 자동 병합 data keepPreviousData 자동 병합 자동 병합 data undefined 가능 previousData 이전 값 유지 ?? 직접 조합 화면에 쓰는 값 = data 화면에 쓰는 값 = data 화면에 쓰는 값 data ?? previousData isPlaceholderData 플래그 제공 isLoading (일반 로딩 플래그) 구분 플래그 없음
React Query와 SWR은 이전 값을 data 안에 합쳐서 넘기는 반면, Apollo는 data와 previousData를 따로 넘겨 ?? 로 직접 조합하게 하고, 이전 값이라는 표시도 React Query만 준다.

실패했을 때의 태도도 갈린다. SWR과 Apollo는 실패해도 이전 값을 화면에 남겨두고 error를 따로 확인하게 하지만, React Query의 placeholderData는 캐시된 값이 아니라 임시 데이터라 실패하는 순간 함께 사라진다.

React Query SWR Apollo 요청 실패 요청 실패 요청 실패 placeholderData 해제 data는 유지 previousData 유지 data: undefined isPlaceholderData: false data 그대로 = 이전 키 값 previousData 그대로 = 이전 값 유지 빈 상태로 바로 드러남 error로 별도 확인 필요 error로 별도 확인 필요
실패 한 번에 React Query만 빈 화면으로 떨어지고, SWR과 Apollo는 이전 값을 그대로 둔 채 실패 신호를 error 쪽으로만 흘려보낸다.

정리하며

React Query, SWR, Apollo는 조회 조건이 바뀌는 순간 화면이 비면서, 로딩 중인 상태를 데이터가 없는 것으로 볼지 이전 데이터가 아직 유효한 것으로 볼지를 판단해야 하는 같은 상황에 놓인다. 세 라이브러리는 이 상황을 previousData / placeholderData / keepPreviousData라는 이름의 같은 장치로 풀어낸다.

다만 로딩 중인 값을 데이터가 없는 것으로 볼지 이전 데이터로 볼지를 누가 판단하느냐는 라이브러리마다 다르다. React Query와 SWR은 이전 값을 자동으로 병합하고 플래그로 구분까지 해주는 반면, Apollo는 값만 넘겨줄 뿐 병합과 판단을 코드 작성자에게 맡긴다. 그래서 Apollo를 쓴다면 같은 자원을 다시 조회하는 화면인지 다른 자원으로 전환하는 화면인지부터 직접 구분해야 previousData를 안전하게 쓸 수 있다.

라이브러리 자동 병합 구분 플래그 실패 시 유지 React Query 자동 있음 사라짐 SWR 자동 없음 유지 Apollo 수동 없음 유지 자동으로 처리되거나 값이 유지됨(초록색) 값이 사라지거나 수동 처리가 필요함(빨간색) 전용 플래그처럼 따로 살펴야 할 지점(파란색) 전용 구분 플래그를 제공하지 않음(점선)
세 기준을 모두 초록색으로 채우는 라이브러리는 없고, React Query는 실패에서, SWR은 구분 플래그에서, Apollo는 병합과 구분 플래그에서 코드 작성자의 손이 더 필요하다.