Tanstack Query(React Query)는 어떻게 온라인, 오프라인을 감지하는 걸까
Tanstack Query (이하 React Query)로 api를 불러오는 화면에서 오프라인으로 테스트하면 에러가 발생안하고 계속해서 로딩상태로 멈추는 걸 경험했다.
찾아보니 React Query가 기본적으로 온라인이 되기 까지 요청을 하지 않는 것을 알게되어 어떻게 감지하는지 궁금해져서 찾아보게 되었다.
공식 문서나 오프라인시 처리에 관한 내용은 아래 글들에서 확인하자.
- v4부터 network mode 가 생겨서 여러가지 오프라인시에 대처를 설정으로 할 수 있게 되었다.
- 어떻게 처리할까는 tkdodo님의 아래 블로그를 참조하자. (스포하자면 fetchStatus로 체크하거나 network mode 설정을 상황에 맞게 잘 설정해야한다)
디깅
현재 최신버전인 v5를 기준으로 찾아본다.
레포지토리에서 networkMode
키워드로 검색하고 파일 확장자를 타입스크립트로 추려보자. query, mutation, queryClient
외에 queryObsever
와 retryer
라는 파일들이 보인다.
queryObsever
는 뭔가 쿼리를 확인하는 쪽인 것 같아 networkMode
를 사용하는 코드를 보면 아래와 같다.
if (fetchOnMount || fetchOptionally) { fetchStatus = canFetch(query.options.networkMode) ? 'fetching' : 'paused' if (!state.dataUpdatedAt) { status = 'pending' } }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/queryObserver.ts#L456
canFetch
라는 함수로 어떠한 플래그를 받고 이후에 fetchStatus
를 fetching | paused
상태로 만들게 된다.
canFetch
가 어디서 왔냐하면 아까 검색에서 걸린 retryer
파일이다. 찾아보자.
export function canFetch(networkMode: NetworkMode | undefined): boolean { return (networkMode ?? 'online') === 'online' ? onlineManager.isOnline() : true }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/retryer.ts#L47
networkMode
가 online
이라면 onlineManager
를 통해 확인하고 아니라면 일반적으로 true
를 리턴하게 된다.
그렇다면, onlineManger
가 관리하고 있을 것 같다 찾아보자. constructor
부분부터 조금씩 알아보자
type Listener = (online: boolean) => void type SetupFn = (setOnline: Listener) => (() => void) | undefined export class OnlineManager extends Subscribable<Listener> { #online = true #cleanup?: () => void #setup: SetupFn constructor() { super() this.#setup = (onOnline) => { // addEventListener does not exist in React Native, but window does // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!isServer && window.addEventListener) { const onlineListener = () => onOnline(true) const offlineListener = () => onOnline(false) // Listen to online window.addEventListener('online', onlineListener, false) window.addEventListener('offline', offlineListener, false) return () => { // Be sure to unsubscribe if a new handler is set window.removeEventListener('online', onlineListener) window.removeEventListener('offline', offlineListener) } } return } }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/onlineManager.ts#L7
기본적으로 Subscribable
을 상속하고 있는데 간단하게 리스너들을 구독시키고 발화시키고 그런 간단한 클래스이다. 물론 실제 발화시키는 내용은 상속 받은 쪽에서 처리하게 된다.
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/subscribable.ts
setup
함수로 윈도우의 온라인과 오프라인 상태에 이벤트리스너를 등록시키는 함수를 가지게 되고, 넘겨주는 onOnline
같은 경우는 아래의 타입을 통해 온라인 플래그를 넘겨주는 콜백이 넘어오는 것이라는 걸 알 수 있다.
type SetupFn = (setOnline: Listener) => (() => void) | undefined
결국 constructor
는 윈도우의 온라인, 오프라인 이벤트를 이벤트 리스너로 등록하고 콜백 함수로 상속받은 listeners
를 발화시키는 함수를 넘겨주는 setup
함수를 만들고 있다.
constructor
에서 무슨일이 일어나는지 알았으니, 우리가 찾아온 코드 onlineManager.isOnline()
의 isOnline
를 들여다 보자.
isOnline(): boolean { return this.#online }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/onlineManager.ts#L66
this.online
을 그대로 사용한다.
this.online
을 직접 변경 시키는 메소드는 setOnline
뿐이다.
setOnline(online: boolean): void { const changed = this.#online !== online if (changed) { this.#online = online this.listeners.forEach((listener) => { listener(online) }) } }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/onlineManager.ts#L55
이 setOnline
을 내부에서 부르고 있는 다른 메소드가 있는데 이게 setEventListener
이다.
setEventListener(setup: SetupFn): void { this.#setup = setup this.#cleanup?.() this.#cleanup = setup(this.setOnline.bind(this)) }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/onlineManager.ts#L49
setup
함수를 받아 다시 인스턴스에 할당하고, cleanup
을 발화 시켜 기존의 setup
함수를 통해 만들어진 로직을 작동시키고 다시 cleanup
함수를 만든다.
그러면 setEventListener
를 어디서 설정하고 있나 찾아보자.
외부에서는 setEventListener
를 직접 부르는 건 없었기에 내부에서 onSubscribe
라는 메소드를 보면 부르고 있는 걸 알 수 있다.
protected onSubscribe(): void { if (!this.#cleanup) { this.setEventListener(this.#setup) } }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/onlineManager.ts#L36
이 onSubscribe
는 상속받은 Subscribable
클래스에서 subscribe
메소드에서 부르게 된다.
subscribe(listener: TListener): () => void { this.listeners.add(listener) this.onSubscribe() return () => { this.listeners.delete(listener) this.onUnsubscribe() } }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/subscribable.ts
그러면 이 subscribe
는 언제 불려질까? 실제 프로젝트에서는 내가 직접 부른적이 없더라도 사용되어지고 있다.
찾아보니 queryClient
의 mount
메소드에서 불려지는 것을 알 수 있다.
mount(): void { this.#mountCount++ if (this.#mountCount !== 1) return this.#unsubscribeFocus = focusManager.subscribe(() => { if (focusManager.isFocused()) { this.resumePausedMutations() this.#queryCache.onFocus() } }) // 여기서 우리가 설정하지 않더라도 자동으로 등록되게 된다. this.#unsubscribeOnline = onlineManager.subscribe(() => { if (onlineManager.isOnline()) { this.resumePausedMutations() this.#queryCache.onOnline() } }) }
https://github.com/TanStack/query/blob/v5.0.0/packages/query-core/src/queryClient.ts#L83
마무리
브라우저의 온라인, 오프라인을 감지하는 이벤트 등록하는 과정과 이러한 과정을 감싸는 옵저버 형태의 클래스 그리고 유저가 쿼리 클라이언트를 등록하는 것만으로 자동으로 설정되게 잘 처리되어 있었다.
onlineManager
는 사실 직접 관리할 수 도 있는데, 예를 들어 React Natvie 같은 경우 이벤트 리스너를 등록 못 하기에 직접 관리하는 법에 대한 문서도 있다.
https://tanstack.com/query/v5/docs/react/reference/onlineManager
조사하면서 onlineManager
클래스의 v4와 v5 코드가 v5로 넘어가면서 꽤 변한 것을 눈치챘는데, v4의 경우 위의 문서 내용처럼 navigator.onLine
을 직접 사용해서 문제가 생긴 것 같다.
다시 한번 React Query의 유용함을 느꼈다. 직접 처리를 할 수도 있지만, 이런 귀찮은 작업을 매번 하기보다 대신 관리해주는게 참 편한 것 같다 🐰