React Query의 useClient 구조를 알아보자
오늘은 저번 주에 이어서 useQuery가 어떻게 실행되며 그에 따라 React가 어떻게 업데이트를 진행하는지 알아보겠습니다!
export default function SomeList() {
const {page, setPage} = useState<number>(0);
const {size, setSize} = useState<number>(10);
const fetchMethod = async () => {
//... API 호출
}
const { data, error } = useQuery({ queryKey: ["someList", page, size], queryFn: fetchMethod });
return (<div>{JSON.stringify(data)}</div>)
}
import { QueryObserver } from '@tanstack/query-core'
import { useBaseQuery } from './useBaseQuery'
import type { DefaultError, QueryClient, QueryKey } from '@tanstack/query-core'
import type {
DefinedUseQueryResult,
UseQueryOptions,
UseQueryResult,
} from './types'
import type {
DefinedInitialDataOptions,
UndefinedInitialDataOptions,
} from './queryOptions'
export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) {
return useBaseQuery(options, QueryObserver, queryClient)
}
useQuery 훅이 호출되면 내부적으로
QueryClientContext
에서QueryClient
인스턴스를 가져옵니다. QueryClient는 모든 쿼리의 상태와 캐시를 관리합니다.
또한, 전달 받은 인자를 통해 useBaseQuery 훅을 호출합니다.
import { useEffect, useRef } from 'react'
import { useSyncExternalStore } from 'use-sync-external-store/shim' // React 18 이전 호환
export function useBaseQuery(observer, options) {
const mountedRef = useRef(false);
// React 외부 상태(QueryObserver)와 동기화
const result = useSyncExternalStore(
// 1. 구독 로직: Query 상태가 변경되면 onStoreChange 호출
(onStoreChange) => {
const unsubscribe = observer.subscribe(notifyManager.batchCalls(onStoreChange))
return unsubscribe;
},
() => observer.getCurrentResult(),
() => observer.getCurrentResult()
);
// observer 옵션 설정 (queryFn, staleTime 등)
useEffect(() => {
mountedRef.current = true;
observer.setOptions(options);
return () => {
mountedRef.current = false;
};
}, [observer, options]);
return result;
}
useBaseQuery를 요약하면 위 코드와 같습니다.
useQuery
훅을 호출하는 각각의 리액트 컴포넌트는 하나의 QueryObserver 인스턴스와 연결됩니다.
연결된 이후 Query의 변화가 발생한다면QueryObserver
는useSyncExternalStore
훅을 사용하여 컴포넌트를 리렌더링 시킵니다.
export class Subscribable<TListener extends Function> {
protected listeners = new Set<TListener>()
constructor() {
this.subscribe = this.subscribe.bind(this)
}
subscribe(listener: TListener): () => void {
this.listeners.add(listener)
this.onSubscribe()
return () => {
this.listeners.delete(listener)
this.onUnsubscribe()
}
}
hasListeners(): boolean {
return this.listeners.size > 0
}
protected onSubscribe(): void {
// Do nothing
}
protected onUnsubscribe(): void {
// Do nothing
}
}
useSyncExternalStore는 컴포넌트의 리렌더링을 진행할 수 있는 onStoreChange 메서드를 인자로 같습니다.
이 onStoreChange는 observer.subscribe와 연결되는데 subscribe 메서드는 QueryObserver의 부모 클래스인 Subscribable에서 확인할 수 있습니다.
export class QueryObserver<
TQueryFnData = unknown,
TError = DefaultError,
TData = TQueryFnData,
TQueryData = TQueryFnData,
TQueryKey extends QueryKey = QueryKey,
> extends Subscribable<QueryObserverListener<TData, TError>> {
//...
protected onSubscribe(): void {
if (this.listeners.size === 1) {
this.#currentQuery.addObserver(this)
if (shouldFetchOnMount(this.#currentQuery, this.options)) {
this.#executeFetch()
} else {
this.updateResult()
}
this.#updateTimers()
}
}
protected onUnsubscribe(): void {
if (!this.hasListeners()) {
this.destroy()
}
}
notify(options: NotifyOptions) {
this.listeners.forEach(listener => {
listener();
});
// Flush batched updates (e.g., to React)
this.queryCache.notify(options);
this.mutationCache.notify(options);
}
//...
}
QueryObserver는 listeners를 순회하여 현재 observer와 연결하고 있는 컴포넌트를 리렌더링 시키게됩니다.
오늘은 useQuery가 QueryObserver를 통해 어떻게 React의 컴포넌트를 리렌더링 시키는지 알아보았습니다. 단순하게 사용만 했었지만 실제로 들어가서 보니까 상태 관리를 어떻게 진행해야하는지, 어떻게 최적화를 할 수 있을지 좀 더 깊이있게 고민할 수 있었던것 같습니다!