어댑터 만들기
어댑터는 fuery_core의 공개 API만으로 Fuery의 쿼리와 뮤테이션을 직접 만든 위젯이나 다른 상태 관리 라이브러리에서 렌더링해요. fuery의 위젯과 fuery_hooks의 훅도 이렇게 만든 어댑터예요. 그래서 직접 만든 어댑터도 이 위젯과 훅이 하는 일을 모두 할 수 있어요.
어댑터는 렌더링하는 쿼리나 뮤테이션마다 슬롯을 하나씩 유지해요. 슬롯(ObserverSlot)은 렌더링할 때마다 바뀔 수 있는 소스의 옵저버를 담아요.
슬롯으로 쿼리 렌더링하기
섹션 제목: “슬롯으로 쿼리 렌더링하기”렌더링할 때마다 update를 호출하고, result를 읽고, 바뀌면 다시 렌더링하도록 구독하세요. flutter_hooks만으로 작성한 이 훅에 슬롯의 규칙이 모두 들어 있어요.
QueryResult<TData> useMyQuery<TData extends Object>(QuerySource<TData> query) { final client = FueryProvider.of(useContext(), listen: true); final slot = useMemoized(() => QuerySlot(query, client)); final changes = useState(0); useEffect(() { var active = true; final unsubscribe = slot.subscribe(notifyManager.batchCalls((_) { if (active) changes.value++; })); return () { active = false; unsubscribe(); slot.dispose(); }; }, [slot]); slot.update(query, client); return slot.result;}슬롯의 규칙
섹션 제목: “슬롯의 규칙”QuerySlot은 QuerySource를 받아요. QuerySource는 Query나 QueryObserver예요. QuerySlot에는 이런 멤버가 있고, InfiniteQuerySlot과 MutationSlot에도 같은 멤버가 있어요.
| 멤버 | 하는 일 |
|---|---|
update(source, client) |
렌더링할 때마다 호출하세요. 정의를 받으면 슬롯이 옵저버를 직접 소유하고 옵저버의 옵션을 업데이트해요. 클라이언트가 바뀌면 새 옵저버를 만들어요. 옵저버를 받으면 슬롯은 그 옵저버를 그대로 사용하고, 클라이언트도 옵저버를 만들 때의 클라이언트를 사용해요. |
result |
렌더링할 결과예요. update가 반환하면 바로 최신 결과가 돼요. |
subscribe(listener) |
이후의 모든 결과로 listener를 호출해요. update가 슬롯을 다른 옵저버로 옮겨도 구독을 유지해요. 리스너를 제거하는 함수를 반환해요. |
listen((previous, current) {...}) |
이후 결과가 바뀔 때마다 리스너를 호출해요. 화면 이동 같은 사이드 이펙트에 사용해요. previous는 리스너에 마지막으로 넘긴 결과이고, 처음에는 시작할 때의 result예요. 리스너를 멈추는 함수를 반환해요. |
dispose() |
모든 리스너를 제거해요. 슬롯이 옵저버를 만들었다면 쿼리 옵저버는 없애고, 뮤테이션 옵저버는 초기 상태로 되돌려요. 이때 가장 최근 mutate 호출의 콜백도 버려요. |
observer |
슬롯이 지금 렌더링에 사용하는 옵저버예요. |
쿼리, 무한 쿼리, 뮤테이션의 리스너 위젯과 컨슈머 위젯, useOnQueryChange, useOnMutationChange는 listen을 호출해요. 그래서 이 규칙을 따라요.
- 리스너는 마이크로태스크에서 실행되고, 렌더링하는 동안에는 실행되지 않아요.
- 시작할 때의
result나, 이전 결과와 같은 결과로는 호출되지 않아요. update가 슬롯을 다른 옵저버로 옮기면, 호출 없이 새result에서 다시 시작해요.listen도 구독해요. 그래서 마운트된 위젯이 있을 때처럼 쿼리가 데이터를 가져와요.- 리스너에서 에러가 발생하면 Fuery가 그 에러를 클라이언트의
onUncaughtError로 전달해요.
리스너 호출 묶기
섹션 제목: “리스너 호출 묶기”QuerySlot, InfiniteQuerySlot, MutationSlot은 subscribe 리스너를 동기로 호출해요. 호출 시점이 다른 위젯이 빌드하는 중일 때도 있어요. 마운트되는 위젯이 데이터를 가져오기 시작할 때가 그 예예요. 렌더링 중에 업데이트할 수 없는 프레임워크에서는 위 훅처럼 하세요.
- 리스너를
notifyManager.batchCalls로 감싸세요. 그러면 변화가 마이크로태스크에서 전달돼요. - 해제한 뒤에 도착한 변화는 무시하세요.
QueriesSlot과 MutationStateSlot은 이미 리스너를 마이크로태스크에서 호출해요. 그래서 두 단계 모두 필요 없어요.
다른 슬롯
섹션 제목: “다른 슬롯”| 슬롯 | 소스 | 결과 |
|---|---|---|
InfiniteQuerySlot |
InfiniteQuerySource: InfiniteQuery나 InfiniteQueryObserver |
InfiniteQueryResult |
MutationSlot |
MutationSource: Mutation이나 MutationObserver |
MutationResult |
QueriesSlot |
데이터 타입이 같은 QuerySource의 리스트 |
같은 순서의 QueryResult 리스트 |
QueriesSlot은 useQueries 같은 훅에 사용해요. subscribe 리스너를 마이크로태스크에서 호출하고, 함께 도착한 변화에는 한 번만 호출해요. 그래서 batchCalls가 필요 없어요. 키가 리스트에 남아 있는 동안에는 리스트 순서가 바뀌어도 쿼리마다 옵저버를 유지해요. observer는 옵저버의 리스트예요. 옵저버를 추가하거나, 제거하거나, 바꾸거나, 옮길 때만 새 리스트가 돼요.
뮤테이션의 모든 실행
섹션 제목: “뮤테이션의 모든 실행”MutationStateSlot은 뮤테이션의 모든 실행(mutate 호출 한 번)의 상태를 알려줘요. 실행을 어디서 시작했든 상관없어요. MutationState 위젯과 useMutationState가 이 슬롯을 사용해요.
MutationStateSource를 받아요.MutationStateSource는mutationKey가 있는Mutation이나MutationFilters예요.- 캐시를 읽기만 해요.
observer는 클라이언트의MutationCache예요. result는 실행의 상태를 오래된 것부터 담은 리스트예요. 조건에 맞는 실행이 추가되거나, 제거되거나, 바뀌기 전까지는 같은 리스트를 유지해요.subscribe리스너를 마이크로태스크에서 묶음마다 한 번, 리스트가 바뀌었을 때만 호출해요. 그래서batchCalls가 필요 없어요.
subscribeToRuns((previous, current) {...})는 조건에 맞는 실행이 바뀔 때마다 리스너를 호출하고, 그 실행의 이전 상태를 previous로 넘겨요. 나중에 시작한 실행이면 previous는 idle 상태예요. 리스너를 추가한 시점의 실행 상태나, 캐시가 제거한 실행은 알리지 않아요. MutationStateListener와 useOnMutationStateChange가 이 메서드를 사용해요.
결과만으로 변화 구독하기
섹션 제목: “결과만으로 변화 구독하기”결과에는 자신을 알린 옵저버가 result.observer로 들어 있어요. 결과만 받은 어댑터는 useOnQueryChange와 useOnMutationChange처럼 그 옵저버로 자기 슬롯을 만들어 변화를 구독해요.
void Function() listenTo<TData extends Object>( QueryResult<TData> result, void Function(QueryResult<TData> previous, QueryResult<TData> current) listener,) { final observer = result.observer; if (observer == null) return () {}; final slot = QuerySlot(observer, observer.client); slot.listen(listener); return slot.dispose;}- 슬롯은 옵저버를 그대로 사용하고, 절대 없애지 않아요.
- 생성자로 만든
QueryResult에서는observer가null이에요. InfiniteQueryResult에는InfiniteQueryObserver가 있고,InfiniteQuerySlot이 이 옵저버를 받아요.
클라이언트 읽기
섹션 제목: “클라이언트 읽기”Flutter에서는 FueryProvider.of(context, listen: true)가 가장 가까운 FueryProvider가 제공하는 클라이언트를 반환하고, 없으면 Fuery.client를 반환해요. 제공하는 클라이언트가 바뀌면 호출한 쪽이 다시 빌드돼요. 그러면 다음 update가 옵저버를 소유한 슬롯을 새 클라이언트로 옮겨요.
Flutter 밖에서는 앱이 사용하는 클라이언트를 넘기세요.
포커스와 재연결 때 다시 가져오기
섹션 제목: “포커스와 재연결 때 다시 가져오기”Flutter에서는 Fuery의 위젯과 훅처럼, 어댑터가 마운트될 때 FueryBinding.ensureInitialized()를 호출하세요. 이 메서드가 앱 생명주기를 연결해요. 그래서 앱이 포그라운드로 돌아오면 Fuery가 stale 상태인 쿼리를 다시 가져오고, 앱이 백그라운드에 있는 동안에는 재시도를 미뤄요. 두 번째 호출부터는 아무것도 하지 않아요. 위에 FueryProvider가 있으면 프로바이더도 이 메서드를 호출해요. 앱이 포그라운드로 돌아올 때를 참고하세요.
마운트된 클라이언트만 앱이 포그라운드로 돌아오거나 네트워크가 다시 연결될 때 데이터를 다시 가져와요. 멈춘 뮤테이션을 이어서 실행하는 것도 마운트된 클라이언트뿐이에요. Fuery.client는 항상 마운트돼 있고, FueryProvider는 자신의 클라이언트를 마운트해요. 어댑터가 그 밖의 클라이언트를 사용하면 mount()를 호출하고, 사용을 멈출 때 unmount()를 호출하세요. Flutter 밖에서는 focusManager.setEventListener로 호스트의 포커스 이벤트를 연결하세요. QueryClient 레퍼런스를 참고하세요.
Fuery 소스 코드에서
섹션 제목: “Fuery 소스 코드에서”adapter_test.dart는 Flutter 없이 슬롯으로 쿼리를 렌더링해요.hooks.dart는fuery_hooks의 모든 훅을 슬롯으로 만들어요.result_subscriber.dart는fuery의 위젯을 슬롯으로 만들어요.