콘텐츠로 이동

캐시가 동작하는 방식

두 화면이 요청 하나를 공유해요. stale 데이터는 Fuery가 알아서 다시 가져와요. 사용하지 않는 데이터는 기본으로 5분이 지나면 메모리에서 사라져요. 이 동작은 표의 구성 요소가 함께 만들어요.

용어 설명 API 타입
쿼리 서버 데이터의 정의. 키, 쿼리 함수, 옵션으로 이뤄져요. 데이터는 담지 않아요. Query, InfiniteQuery
뮤테이션 서버 데이터를 바꾸는 일의 정의. 뮤테이션 함수와 옵션으로 이뤄져요. 상태는 담지 않아요. Mutation, NoVariablesMutation
클라이언트 쿼리 캐시와 뮤테이션 캐시를 소유하는 객체. QueryClient
캐시 항목 한 클라이언트에서 키 하나의 데이터와 상태. CachedQuery
옵저버 정의와 클라이언트 하나를 잇는 객체. 쿼리 옵저버는 캐시 항목 하나를 지켜봐요. 뮤테이션 옵저버는 실행을 시작하고 가장 최근 실행을 알려요. QueryObserver, InfiniteQueryObserver, MutationObserver
결과 옵저버가 알리는 값. 옵저버가 보는 상태와 refetch나 mutate 같은 액션을 담아요. QueryResult, InfiniteQueryResult, MutationResult
실행 mutate 호출 한 번과 그 변수, 상태. CachedMutation

정의는 무엇을 가져오거나 바꿀지만 말해요. Query는 키, 쿼리 함수, 옵션을 담아요. Mutation은 뮤테이션 함수와 옵션을 담아요. 둘 다 데이터나 클라이언트는 담지 않아요. 정의를 만들어도 아무것도 시작하지 않아요.

그래서 정의는 최상위 값으로 두거나, 함수나 build에서 만들어도 돼요.

final todosQuery = Query(
queryKey: ['todos'],
queryFn: (_) => api.getTodos(),
);
Query<Todo> todoQuery(int id) => Query(
queryKey: ['todos', id],
queryFn: (_) => api.getTodo(id),
);

todoQuery(1)을 두 번 호출하면 객체가 두 개 생겨요. 두 객체의 키는 모두 ['todos', 1]이에요. Fuery는 키를 값으로 비교해서 두 객체는 캐시 항목 하나를 사용해요. 키에 담을 수 있는 값은 쿼리 키에 있어요.

QueryClient는 캐시하는 모든 것을 캐시 두 개에 나눠 담아요.

  • QueryCache는 키마다 캐시 항목을 하나씩 담아요.
  • MutationCache는 mutate 호출마다 실행을 하나씩 담아요.

클라이언트에는 캐시가 사용하는 기본값과 스토리지도 있어요.

Fuery.client는 기본 클라이언트예요. 처음 사용할 때 Fuery가 만들어요. FueryProvider는 하위 트리에 별도 클라이언트를 줘요. 그 아래 위젯은 그 클라이언트를 사용해요. 클라이언트마다 캐시가 따로 있어요. 그래서 같은 키라도 클라이언트가 다르면 캐시 항목이 두 개이고, 데이터도 따로예요. 위젯, observe(), 쿼리 함수가 어떤 클라이언트를 사용하는지는 쿼리가 사용하는 클라이언트에 있어요.

옵저버는 정의를 한 클라이언트의 캐시 항목에 연결해요. 옵저버를 만드는 방법은 두 가지예요.

  • 정의를 받은 위젯이나 훅은 마운트된 동안 옵저버 하나를 유지해요.
  • observe()는 Cubit이나 서비스처럼 위젯 밖의 코드에서 옵저버를 만들어요. build에서 호출하지 말고 한 번만 호출하세요. 호출할 때마다 새 옵저버가 생겨요.

옵저버는 이런 일을 해요.

  • 구독해요. 옵저버는 첫 리스너가 생기면 캐시 항목을 구독해요. 첫 리스너는 마운트된 위젯이나 훅일 수도 있고, observe()로 만든 옵저버의 stream.listen 호출일 수도 있어요. 캐시 항목에 데이터가 없거나 데이터가 stale 상태면 데이터를 가져와요(두 번째 경우는 refetchOnMount가 정해요).
  • 가져오기를 공유해요. 캐시 항목이 데이터를 가져오는 중에 구독한 옵저버는 이미 시작된 가져오기에 합류해요. 함께 열린 두 화면은 요청을 한 번만 보내요.
  • 구독을 해제해요. 마지막 리스너가 떠나면 옵저버는 구독을 해제해요. 위젯이 언마운트되거나 스트림 구독을 취소할 때가 그래요.
  • 자기 옵션을 적용해요. 옵저버마다 자기 staleTime과 refetchOnMount를 적용해요. 두 화면이 캐시 항목 하나를 지켜보면서도, 데이터가 stale 상태인지는 화면마다 따로 판단해요.
  • 캐시 항목이 남는 시간을 정해요. 옵저버마다 gcTime(가비지 컬렉션 시간)을 요청해요. gcTime은 지켜보는 옵저버가 하나도 없을 때 캐시 항목이 메모리에 남는 시간이에요. 캐시 항목은 옵저버나 가져오기가 요청한 값 중 가장 긴 시간을 유지해요.
  • 키를 따라가요. 위젯이 다른 키의 정의를 받아 다시 빌드하면 옵저버는 그 키의 캐시 항목으로 옮겨가요.
  • 결과를 알려요. 옵저버는 무언가 바뀔 때마다 결과를 알려요. 결과는 캐시 항목의 상태를 옵저버의 옵션에 비춰 보여줘요. 예를 들어 isStale은 그 옵저버의 staleTime으로 판단해요. 필드 목록은 QueryResult 필드에 있어요.

쿼리를 관찰하는 두 가지 방법은 쿼리 사용하기에 있어요. 옵저버를 유지하는 위젯 목록은 위젯에 있어요.

캐시 항목은 이 단계를 거쳐요. 3단계와 4단계는 옵저버마다 달라요. 옵저버마다 자기 staleTime으로 fresh 상태인지 판단하기 때문이에요.

단계 일어나는 일 끝나는 조건
1. 만들어짐 키의 첫 옵저버나 첫 client.query 호출이 데이터 없는 캐시 항목을 만들어요. 옵저버가 구독하거나 client.query가 데이터를 가져와요.
2. pending 쿼리 함수가 실행돼요. status는 pending이에요. fetchStatus는 fetching이에요. 재시도하려고 앱이 포그라운드로 돌아오기를 기다리는 동안, 그리고 연결 상태 소스가 있으면 기기가 오프라인인 동안에는 fetchStatus가 paused예요. 데이터가 도착하거나(success), 재시도 끝에 가져오기가 실패해요(error).
3. fresh 데이터를 받은 지 staleTime이 지나지 않았어요(기본값은 0이라서, 직접 설정하지 않으면 데이터는 이 단계를 건너뛰어요). 기본으로 마운트, 포커스, 재연결이 일어나도 다시 가져오지 않아요. staleTime이 지나거나 invalidateQueries가 데이터를 stale 상태로 표시해요.
4. stale 데이터는 화면에 남아요. 옵저버가 구독할 때, 앱이 포그라운드로 돌아올 때, 연결 상태 소스가 있으면 네트워크가 다시 연결될 때 Fuery가 백그라운드에서 다시 가져와요. 다시 가져와서 새 데이터가 들어오면 3단계로 돌아가요.
5. 비활성 캐시 항목을 지켜보는 옵저버가 없어요. 마지막 옵저버가 떠났거나, client.query나 setData로만 채운 캐시 항목처럼 처음부터 옵저버가 없었어요. 데이터는 메모리에 남아 있어서, 화면이 돌아오면 바로 보여줘요. 옵저버가 구독하거나(3단계나 4단계로 돌아가요) gcTime이 지나요.
6. 제거됨 옵저버가 없는 채로 gcTime(기본값: 5분)이 지나면 Fuery가 캐시 항목을 제거해요. 기기에 저장한 데이터는 스토리지에 남아요. 그 키를 다음에 사용하면 1단계부터 다시 시작하고, 저장된 데이터가 있으면 복원해요.

이 과정에서 이런 일도 일어나요.

  • initialData나 setData로 만들었거나 저장된 데이터에서 만든 캐시 항목은 데이터가 있는 상태로 3단계나 4단계에서 시작해요.
  • 옵저버가 시작한 가져오기는 기본으로 1초, 2초, 4초를 기다리며 3번 재시도해요. client.query는 쿼리나 클라이언트의 기본값(DefaultOptions, setQueryDefaults)이 retry를 설정할 때만 재시도해요.
  • 첫 가져오기가 실패하면 캐시 항목은 데이터 없이 error 상태로 남아요. 이 캐시 항목은 옵저버가 구독할 때(retryOnMount가 false가 아니면) 다시 가져오고, stale 데이터처럼 포커스와 재연결 때도 다시 가져와요.
  • 다시 가져오다가 실패해도 데이터는 남아요. status는 error가 되고, 데이터는 stale 상태 그대로예요. 그래서 다음 트리거에 다시 가져와요.
  • invalidateQueries는 조건에 맞는 캐시 항목을 stale 상태로 표시하고, 그중 옵저버가 지켜보는 캐시 항목을 다시 가져와요.
  • refetchInterval은 이 옵션을 설정한 켜진 옵저버가 구독하는 동안 타이머로 다시 가져와요. refetchIntervalInBackground가 true가 아니면 앱이 백그라운드에 있는 동안 멈춰요.
  • staleTime: staticStaleTime을 설정한 옵저버에게는 invalidateQueries 뒤에도 데이터가 계속 3단계에 머물러요.
  • removeQueries와 clear()는 캐시 항목을 바로 제거하고, 저장된 데이터도 삭제해요.
  • 다시 가져온 데이터가 캐시된 데이터와 같으면 Fuery는 이전 객체를 유지해요. 그래서 이 객체를 비교하는 위젯은 다시 빌드하지 않아요. 리스트가 바뀌었을 때는 같은 인덱스의 이전 항목과 같은 항목마다 이전 객체를 유지해요. 직접 만든 클래스는 ==를 구현해야 같은 값으로 봐요. 리스트에서 어떻게 동작하는지는 바뀐 부분만 다시 빌드하기에 있어요.

다시 가져오기 트리거마다 refetchOnFocus 같은 옵션이 있어요. 옵션과 기본값은 쿼리 옵션에 있어요.

뮤테이션은 쿼리처럼 캐시 항목을 공유하지 않아요. mutate를 호출할 때마다 뮤테이션 캐시에 실행이 하나 생겨요. 실행마다 변수와 상태가 따로 있어요. 실행은 정의나 위젯이 아니라 클라이언트의 캐시에 속해요.

  • 정의에서 실행해요. addTodo.mutate('Buy milk')는 Fuery.client의 캐시에, 또는 넘긴 클라이언트의 캐시에 실행을 추가해요. 이 실행은 어떤 옵저버에도 속하지 않아요.
  • 가장 최근 실행을 보여줘요. MutationResult는 그 옵저버가 시작한 가장 최근 실행을 보여줘요. MutationBuilder 하나가 시작한 실행이 그 예예요. 그 결과에서 mutate를 다시 호출하면 새 실행이 이전 실행을 대신하고, reset()은 결과를 idle 상태로 되돌려요.
  • 실행을 따로 둬요. 같은 뮤테이션의 MutationBuilder 두 개는 저마다 옵저버를 유지하고, 자기가 시작한 실행만 보여줘요. 여러 위젯이 옵저버 하나를 사용해야 하는 경우는 옵저버 하나 공유하기에 있어요.
  • 모든 실행을 찾아요. MutationStateBuilder, MutationStateListener, MutationStateSelector, useMutationState는 정의의 mutationKey로, 또는 MutationFilters 조건으로 실행을 모두 찾아요. 실행을 어디서 시작했든 상관없어요. 뮤테이션의 모든 실행 보여주기를 참고하세요.
  • 재시도해요. 실행은 뮤테이션이나 클라이언트의 뮤테이션 기본값(DefaultOptions, setMutationDefaults)이 retry를 설정할 때만 재시도해요.
  • 캐시에서 사라져요. 옵저버가 보여주는 동안 실행은 캐시에 남아요. 실행이 끝나고 보여주는 옵저버가 없으면 Fuery는 gcTime(기본값: 5분)이 지난 뒤 실행을 제거해요. 정의에서 시작한 실행은 옵저버가 없어서 끝나고 gcTime이 지나면 사라져요. client.clear()는 모든 실행을 바로 제거해요.

MutationResult와 MutationState의 필드는 뮤테이션 결과에 있어요.