본문 바로가기

TanStack Query의 FocusManager는 무엇일까? (근데 이제 refetchOnWindowFocus 옵션을 곁들인)

by mugglim 2024. 10. 26.

글을 들어가며

본 글에서는 TanStack Query에서 제공하는 FocusManager를 기반으로, useQuery의 refetchOnWindowFocus 옵션의 원리를 소개하고자 한다.

FocusManager는 무엇인가?

The FocusManager manages the focus state within TanStack Query 
- TanStack Query 공식문서

FocusManager는 TanStack Query 내부에서 사용되는 focus 상태를 관리하는 대상으로 설명되어 있다.

FocusManager는 어떻게 구현되어 있을까?

FocusManager 클래스로 구현되어 있는 객체이다. FocusManager 클래스는 옵저버 패턴을 기반으로 구현되어 있으며, Subscribable 클래스를 상속받고 있다. 옵저버 패턴의 책임 중 하나인, 구독과 관련된 책임은 Subscribable 클래스 내부에서 담당하고 있다.

class FocusManager extends Subscribable<Listener> {
    #focused?: boolean
    #cleanup?: () => void
    constructor();
    protected onSubscribe(): void;
    protected onUnsubscribe(): void;
    setEventListener(setup: SetupFn): void;
    setFocused(focused?: boolean): void;
    onFocus(): void;
    isFocused(): boolean;
}

FocusManager 객체의 책임을 간단히 정리해 보자.

  1. 최신 focus 상태를 알고 있다.
  2. focus 상태의 변경 여부를 감지할 수 있다.
  3. 외부에서 foucs 상태가 변경될 때 이벤트 감지를 위해 구독 기능을 제공한다.
  4. focus 상태가 변경될 때 구독자(Subscriber)들에게 focus 상태를 전달할 수 있다.

FocusManager는 어떻게 focus 상태의 변경 여부를 감지할 수 있을까?

FocusManager는 기본적으로 visibilitychange 이벤트를 사용하여, focus 상태의 변경 여부를 감지한다. visibilitychange 이벤트가 발생될 때마다 document?.visibilityState !== 'hidden' 값을 통해 focus 상태를 변화시킨다.

FocusManager는 클래스의 생성자 함수 쪽 코드를 살펴보면,this.#setup 변수에 visibilitychange 이벤트에 구독시키는 콜백 함수를 할당한다. 이 시점에는 visibilitychange 이벤트에 콜백 함수를 실행하지는 않고, 콜백 함수를 할당만 해둔 상태이다. 

visibilitychange 이벤트에 콜백 함수는 아래의 경우에 등록된다.

  • (1) FocusManager 구독자 추가 이벤트가 발생했을 때, onSubscribe 메소드가 실행된다.
  • (2) setEventListener 메소드 실행 후 #setup 변수가 실행된다.
    • 외부에서 setEventListener 실행하지 않았다면, #setup의 visibilitychange 이벤트를 구독하는 기본 콜백 함수가 구독된다.
    • 이후 구독에는 #cleanup 변수가 할당되어 있어서, #setup 함수가 중복으로 실행되지는 않는다.
export class FocusManager extends Subscribable<Listener> {
  #cleanup?: () => void;
  #setup: SetupFn;

  constructor() {
    super();
    this.#setup = (onFocus) => {
      if (!isServer && window.addEventListener) {
        const listener = () => onFocus();
        window.addEventListener("visibilitychange", listener, false);

        return () => {
          window.removeEventListener("visibilitychange", listener);
        };
      }
      return;
    };
  }
}

만약 visibilitychange 이벤트를 사용할 수 없는 환경이거나 다른 방법으로 focus 여부를 감지하고 싶다면, setEventListener 메소드를 사용하면 된다. 관련된 내용을 공식문서에서도 가이드를 제공하고 있다.

어떤 객체가 FocusManager를 구독할까?

FocusManager 객체는 focus 상태가 변경될 때 마다 구독자들에게 새로운 focus 상태를 전파한다.
그러면 어떤 객체가 FocusManager를 구독하고 있을까?

QueryClient와 FocusManager

QueryClient 객체는 focus 상태를 관리하기 위해, FocusManager를 구독한다. QueryClient는 focus 상태가 false에서 true로 변경된 경우, QueryCache 객체의 onFocus 메소드를 실행한다. 이후 QueryObserver는 refetchOnWindowFocus 옵션 여부에 따라 refetch를 발생시킨다.

// https://github.com/TanStack/query/blob/main/packages/query-core/src/queryClient.ts#L80-L85

class QueryClient {
  mount(): void {
    this.#unsubscribeFocus = focusManager.subscribe(async (focused) => {
      if (focused) {
        await this.resumePausedMutations();
        this.#queryCache.onFocus();
      }
    });
  }
}

FocusManager와 refetchOnWindowFocus 옵션의 연관 관계를 조금만 더 살펴보자.
이해를 돕기 위해 FocusManager가 관리하고 있는 focus 상태가 false에서 true로 변경 된 예시를 가정해보자.

(1) FocusManager -> QueryClient -> QueryCache

  • FocusManager는 true인 focus 상태를 QueryClient에 전달하고, QueryClient는 QueryCache.onFocus() 메소드를 호출한다.
// https://github.com/TanStack/query/blob/main/packages/query-core/src/queryClient.ts#L80-L85
class QueryClient {
  mount(): void {
    // ...
    this.#unsubscribeFocus = focusManager.subscribe(async (focused) => {
      // 전달받은 focused 상태가 true 인 경우에만 아래 구문이 실행된다.
      if (focused) {
        await this.resumePausedMutations();
        // 아래 구문이 실행된다.
        this.#queryCache.onFocus();
      }
    });
  }
}

(2) QueryCache -> Query

  • QueryCache는 구독 중인 Query들에게 Query.onFocus() 메소드를 호출한다.
// https://github.com/TanStack/query/blob/main/packages/query-core/src/queryCache.ts#L208-L214

export class QueryCache extends Subscribable<QueryCacheListener> {
  onFocus(): void {
    notifyManager.batch(() => {
      this.getAll().forEach((query) => {
        query.onFocus();
      });
    });
  }
}

(3) Query -> QueryObserver

  • Query는 구독 중인 QueryObserver 중 refetch를 발생시켜야 하는 QueryObserver를 찾아, refetch를 발생시킨다.
  • refetch를 발생시켜야 하는 QueryObserver는 QueryObserver.shouldFetchOnWindowFocus()이 true인 객체이다.
// https://github.com/TanStack/query/blob/main/packages/query-core/src/query.ts#L276-L283

export class Query {
  onFocus(): void {
    // shouldFetchOnWindowFocus를 기반으로 refetch를 발생시킬 QueryObserver를 필터링한다.
    const observer = this.observers.find((x) => x.shouldFetchOnWindowFocus());

    observer?.refetch({ cancelRefetch: false });

    this.#retryer?.continue();
  }
}

(3 - 참고) QueryObserver.shouldFetchOnWindowFocus 반환값은 무엇인가?

  • QueryObserver.shouldFetchOnWindowFocus의 결괏값은 boolean 이다.
  • 반환값을 계산할 때 shouldFetchOn() 함수를 호출한다. shouldFetchOn 함수는 인자 값에 따라 동일하게 boolean를 반환한다.
  • shouldFetchOn 함수가 true/false를 반환하는 경우는 아래와 같다.
    1. false를 반환하는 경우
      • enabled 옵션이 false 인 경우
    2. true 반환하는 경우
      • refetchOnWindowFocus 옵션이 문자열 'always'인 경우
      • refetchOnWindowFocus 옵션이 true 이면서, stale 상태가 된 query 인 경우
// https://github.com/TanStack/query/blob/main/packages/query-core/src/queryObserver.ts#L120-L126
export class QueryObserver {
  shouldFetchOnWindowFocus(): boolean {
    return shouldFetchOn(
      this.#currentQuery,
      this.options,
      this.options.refetchOnWindowFocus
    );
  }
}

// https://github.com/TanStack/query/blob/main/packages/query-core/src/queryObserver.ts#L712-L725
function shouldFetchOn(query, options, field) {
    if (options.enabled !== false) {
      const value = typeof field === 'function' ? field(query) : field

      return value === 'always' || (value !== false && isStale(query, options))
    }
    return false
  }

위 예시를 살펴보면, refetch를 발생시키는 FocusManager 객체와 QueryObserver 객체는 직접적인 의존성은 없다. 하지만 refetch를 위한 이벤트의 시작은 FocusManager 객체임을 알 수 있다.

글을 마무리 하며

본 글에서 살펴본 내용을 간단히 정리해보자.

  • FocusManager는 TanStack Query 내부에 사용 될 focus 상태를 관리하는 객체이다.
    • TanStack Query는 변경된 focus 상태를 기반으로 refetch를 발생시킨다.
  • 기본적으로 FocusManager는 focus 상태를 visibilitychange 이벤트를 기반으로 감지하며, document?.visibilityState로 foucs 여부를 평가한다.
    • visibilitychange가 아닌 다른 이벤트를 사용하고 싶은 경우, setEventListener 메소드를 사용해야한다.
  • focus 상태가 false에서 true로 변경된 경우, "FocusManager -> QueryCache -> Query -> QueryObserver" 순서로 이벤트가 전달된다.

댓글