
우아한 테크코스 Level1을 하면서, 바닐라 JS에 대한 이해도가 조금 생긴 것 같다.
이 과정에서 웹을 구성하는 핵심이 무엇일까?, 바닐라JS로 웹을 구현하는데 핵심이 무엇일까? 생각해 보았을때, 나는 ‘비동기로 실행되는 이벤트’라고 생각했다. 즉, 상태가 계속해서 변할 때 이벤트를 어떻게 효율적으로 실행하는가? 이다.
리액트에서는 useEffect 와 useState 와 더불어, Redux React Query등을 사용하며, ‘상태를 감지하는 시스템’을 통해 간편하게 관리를 하지만, 바닐라에서는 어떻게 구현하면 좋을까? 생각을 많이 했다.
그 중에서 눈에 띄었던 기능은 ‘웹 컴포넌트’에서의 observedAttributes 메서드, 그리고 Mutation Observer를 통해서 DOM에 있는 요소를 상태 감지하여 함수를 실행할 수 있었던 것이다.
Using custom elements - Web APIs | MDN
One of the key features of web components is the ability to create custom elements: that is, HTML elements whose behavior is defined by the web developer, that extend the set of elements available in the browser.
developer.mozilla.org
MutationObserver - Web APIs | MDN
The MutationObserver interface provides the ability to watch for changes being made to the DOM tree. It is designed as a replacement for the older Mutation Events feature, which was part of the DOM3 Events specification.
developer.mozilla.org
이것 외로 Intersection Observer API 와 같이, ‘Observer’라는 용어를 많이 볼 수 있었고, 웹에서 이벤트를 발생시키는 좋은 아이디어라고 생각되어, 이번 기회에 ‘옵저버 패턴’그리고 리액트의 상태관리 라이브러리인 ‘Tanstack Query(React Query)’를 파헤쳐 보기로 했다!!
Intersection Observer API - Web APIs | MDN
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
developer.mozilla.org
우선 옵저버 패턴이 무엇인지에 대해서 알아보도록 하자.
❓옵저버 패턴은 무엇인가
- 말 그대로 Observer, 즉 관찰자를 생성하여 '관찰' 하는 것.
- 주체가 이벤트를 실행하지 않는다. 즉, 이벤트를 주체에게 전달하는 것이 아니라, 옵저버(관찰자) 객체로 전달해주고(notify), 이 옵저버 객체가 각각의 주체에 이벤트를 실행(update)한다.
- 주로 ‘분산 이벤트 핸들링 시스템’에서 사용
- 프로그래밍적으로 옵저버 패턴은 사실 '관찰' 하기 보단 갱신을 위한 힌트 정보를 '전달' 받길 기다린다고 보는 것이 적절하다. (이 부분에서 헷갈렸음. 옵저버가 어떻게 대상의 변화를 감지할 수 있는가? 에 대한 해답이 풀림)
- 관찰을 당하는 대상, Subject. 그리고 관찰자(구독자, 이벤트 알림 수신자의 역할), Observer로 구성된다.
- 옵저버 패턴를 그림으로 본다면 다음과 같이 구성된다.


(그림에 표시된 update는 update함수가 아닌, 사용자로 인한 state의 동적 변화를 나타낸 것이다.. 동적인 변화를 옵저버에게 알리면, 옵저버가 주체의 update함수를 실행한다.)
- 옵저버 패턴에서, Observer와 Subject는은 다음과 같은 기본 구조를 가진다.
- Observer내부에서 Subject의 update함수를 실행하고, Observer의 notify함수는 Subject에서 실행되는 구조.
- 즉 Subject는 Observer에게 notify하고, Observer는 Subject의 이벤트를 감지하여 update한다.

❓그래서 옵저버 패턴을 왜 쓸까요?
- 여러개의 분산 이벤트를 '구독' 할 수 있다는 것.
- 이 '구독' 시스템을 통해, '원하는 주체'에만 이벤트를 실행시킬 수 있다는 것
- 그리고 주체 (subject)에서 직접 이벤트를 실행하는 것이 아니기 때문에, 느슨한 결합을 유지하여 확장성이 용이하다는 것
- 이는 분산 이벤트의 수가 많아질 수록 빛을 바라는 패턴이라고 생각이 들었다.
아래 링크를 통해 옵저버 패턴을 더 자세히 알아보도록 하자. 개인적으로 제일 이해하기 쉬웠던 참조이다.
옵서버 패턴
/ 디자인 패턴들 / 행동 패턴 옵서버 패턴 다음 이름으로도 불립니다: 이벤트 구독자, 경청자, Observer 의도 옵서버 패턴은 당신이 여러 객체에 자신이 관찰 중인 객체에 발생하는 모든 이벤트에
refactoring.guru
❗React Query에서의 옵저버 패턴
리액트 쿼리의 구조는 다음과 같다.
query-core/src
├── notifyManager.ts
├── query.ts
├── queryCache.ts
├── queryClient.ts
└── queryObserver.ts
react-query/src
├── useBaseQuery.ts
└── useQuery.ts
- Tanstack Query 의 기본적인 로직을 담당하는 query-core/src 소스파일이 있고, 리액트 훅을 담당하는 코드인 react-query/src 소스파일이 있다.
- React Query에서의 주체(subject)는 Query클래스 이다.
- Observer는 QueryObserver클래스 이다.
- 한눈에 간단하게 파일의 구조를 본다면 다음과 같다.

- React Query 에서의 옵저버 인터페이스는 Subscribable클래스 으로 구성된다.
- Subscribable 클래스는 원하는 주체를 저장하는 listeners[] 필드가 있다.
- Subject의 인터페이스는 Removable클래스로 구성된다. (Removable은 gcTime을 관리한다.)
- gcTime은 가비지컬렉팅 타임이다. 즉 gcTime을 통해 remove하는기능을 가진다. (비유를 한다면 구독 해지 정도가 되겠다)

😊주체 subject(Query 클래스)는 다음과 같다.
// Subject ...
addObserver(observer: QueryObserver<any, any, any, any, any>): void {
if (!this.#observers.includes(observer)) {
//...
this.#cache.notify({ type: 'observerAdded', query: this, observer })
}
}
removeObserver(observer: QueryObserver<any, any, any, any, any>): void {
//...
if (!this.#observers.length) {
//...
}
this.#cache.notify({ type: 'observerRemoved', query: this, observer })
}
}
// ...
#dispatch(action: Action<TData, TError>): void {
const reducer = (
state: QueryState<TData, TError>,
): QueryState<TData, TError> => {
switch (action.type) {
case 'failed':
return {
...state,
fetchFailureCount: action.failureCount,
fetchFailureReason: action.error,
}
case 'pause':
return {
...state,
fetchStatus: 'paused',
}
case 'continue':
// ...
case 'fetch':
// ...
case 'success':
// ...
case 'error':
// ...
// ...
}
this.state = reducer(this.state)
notifyManager.batch(() => {
this.#observers.forEach((observer) => {
observer.onQueryUpdate()
// Query에서 옵저버에게 notify한다. 이후 옵저버의 update함수가 실행된다.
})
this.#cache.notify({ query: this, type: 'updated', action })
})
}
}
- Query의 state가 update될 때 실행해야 될 update함수는 dispatch이다.
- 내부적으로 reducer함수가 적용되어 있는 것을 볼 수 있었다.
😊관찰자 observer(QueryObserver)는 다음과 같다.
onQueryUpdate(): void {
this.updateResult()
if (this.hasListeners()) {
this.#updateTimers()
}
}
// ...
setOptions(
options: QueryObserverOptions<
TQueryFnData,
TError,
TData,
TQueryData,
TQueryKey
>,
notifyOptions?: NotifyOptions,
): void {
const prevOptions = this.options
const prevQuery = this.#currentQuery
this.options = this.#client.defaultQueryOptions(options)
if (
this.options.enabled !== undefined &&
typeof this.options.enabled !== 'boolean'
) {
throw new Error('Expected enabled to be a boolean')
}
this.#updateQuery()
this.#currentQuery.setOptions(this.options); // 옵저버에서 Query의 로직이 실행됨
}
- Query에는 state를 변경하는 로직이 있다. 이 로직에 내부적으로 주체의 dispatch함수가 실행된다.
- 그런데 Query에서는 이 state변경 로직을 실행하진 않는다. Observer의 함수를 실행할 뿐이다.
- Query의 내부 state변경 로직은 Observer에서 실행된다. 즉 옵저버의 notify함수 내부에서 실행된다.
자세한 코드는 아래 Tanstack Query 깃 레포에서!
query/packages/query-core/src at main · TanStack/query
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query. - TanStack/query
github.com
❗React 환경에서 view에 렌더링 되기까지.
- 이 부분은 useQuery 훅과 useBaseQuery훅이 관여한다.
- useQuery는 단순히 useBaseQuery를 호출한다.
// useQuery
export function useQuery(options: UseQueryOptions, queryClient?: QueryClient) {
return useBaseQuery(options, QueryObserver, queryClient)
}
// useBaseQuery
const [observer] = React.useState(
() =>
new Observer<TQueryFnData, TError, TData, TQueryData, TQueryKey>(
client,
defaultedOptions,
),
)
const result = observer.getOptimisticResult(defaultedOptions)
React.useSyncExternalStore(
React.useCallback(
(onStoreChange) => {
const unsubscribe = isRestoring
? () => undefined
: observer.subscribe(notifyManager.batchCalls(onStoreChange))
// Update result to make sure we did not miss any query updates
// between creating the observer and subscribing to it.
observer.updateResult()
return unsubscribe
},
[observer, isRestoring],
),
() => observer.getCurrentResult(),
() => observer.getCurrentResult(),
)
React.useEffect(() => {
// Do not notify on updates because of changes in the options because
// these changes should already be reflected in the optimistic result.
observer.setOptions(defaultedOptions, { listeners: false })
}, [defaultedOptions, observer])
// Handle suspense
if (shouldSuspend(defaultedOptions, result)) {
// Do the same thing as the effect right above because the effect won't run
// when we suspend but also, the component won't re-mount so our observer would
// be out of date.
throw fetchOptimistic(defaultedOptions, observer, errorResetBoundary)
}
자세하게 보려면 조금 어렵지만, 가볍게 본다면 다음과 같을 것이다.
- useQuery를 실행하면, useBaseQuery가 실행된다. 이 때 매개변수로 useQeury의 옵션과 옵저버, QueryClient가 들어간다.
- useBaseQuery 내부에서 새로운 옵저버 인스턴스를 만든다. (매개변수로 받은 Obeserver 클래스로 생성)
- 옵저버를 통해 Query 클래스의 데이터를 감지하고, 옵저버 내부의 update함수를 실행한다.
- 이 과정에서 리액트 18버전에 추가된 훅인 useSyncExternalStore가 관여된다.
자세한 코드는 아래 Tanstack Query 깃 레포에서!
query/packages/react-query/src at main · TanStack/query
🤖 Powerful asynchronous state management, server-state utilities and data fetching for the web. TS/JS, React Query, Solid Query, Svelte Query and Vue Query. - TanStack/query
github.com
리액트 쿼리를 이해하는데 도움이 되었던 참조
React Query의 구조와 useQuery 실행 흐름 살펴보기 | 카카오엔터테인먼트 FE 기술블로그
함성준(kevin) 개발에는 인생(喜怒哀樂)이 담겨있습니다. 커피 좋아합니다.
fe-developers.kakaoent.com
'개발관련 > React' 카테고리의 다른 글
[React] Context를 알아보자. (0) | 2024.04.30 |
---|---|
React에서의 MSW 기본 사용법 (0) | 2024.04.14 |
React-query, SWR을 사용해야하는 이유. 엄격 모드 (0) | 2024.01.15 |
[React] React Query와 SWR의 차이? (0) | 2023.11.29 |
[React] SWR이 무엇인가요? (1) | 2023.11.24 |