Tanstack Query(React Query)는 어떻게 온라인, 오프라인을 감지하는 걸까

Tanstack Query(React Query)로 api를 불러오는 화면에서 오프라인으로 테스트하면 에러가 발생안하고 계속해서 로딩상태로 멈추는 걸 경험했다. 찾아보니 기본적으로 온라인이 되기 까지 요청을 하지 않는 것을 알게되어 어떻게 감지하는지 궁금해져서 찾아보게 되었다.
2023.11.15

Tanstack Query (이하 React Query)로 api를 불러오는 화면에서 오프라인으로 테스트하면 에러가 발생안하고 계속해서 로딩상태로 멈추는 걸 경험했다.

찾아보니 React Query가 기본적으로 온라인이 되기 까지 요청을 하지 않는 것을 알게되어 어떻게 감지하는지 궁금해져서 찾아보게 되었다.

공식 문서나 오프라인시 처리에 관한 내용은 아래 글들에서 확인하자.

디깅

현재 최신버전인 v5를 기준으로 찾아본다.

레포지토리에서 networkMode 키워드로 검색하고 파일 확장자를 타입스크립트로 추려보자. query, mutation, queryClient외에 queryObseverretryer 라는 파일들이 보인다.

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 라는 함수로 어떠한 플래그를 받고 이후에 fetchStatusfetching | 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

networkModeonline이라면 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는 언제 불려질까? 실제 프로젝트에서는 내가 직접 부른적이 없더라도 사용되어지고 있다.

찾아보니 queryClientmount 메소드에서 불려지는 것을 알 수 있다.

  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의 유용함을 느꼈다. 직접 처리를 할 수도 있지만, 이런 귀찮은 작업을 매번 하기보다 대신 관리해주는게 참 편한 것 같다 🐰