캐시가 동작하는 방식
두 화면이 요청 하나를 공유해요. 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의 필드는 뮤테이션 결과에 있어요.
다음 단계
섹션 제목: “다음 단계”- 쿼리: 키, fresh 상태, 서로 의존하는 쿼리
- 위젯: 옵저버를 유지하는 위젯
- 뮤테이션: 뮤테이션을 실행하고 그 실행을 보여주는 방법
- 캐시 읽고 업데이트하기: 클라이언트로 캐시 항목을 읽고, 쓰고, 무효화하기
- 쿼리 옵션: 모든 옵션과 기본값