TanStack Query, 직접 구현해보기(1) - 왜 직접 구현해보게 되었나?

재영·2025년 6월 28일
10
post-thumbnail

개요

이 글에서는 TanStack Query를 직접 구현하게 된 배경과 그 과정에서 얻게 된 인사이트를 공유한다.

우아한테크코스에서 라이브러리 없이 미션을 수행하면서, 라이브러리를 사용하지 않았을 때의 불편함을 절실히 느꼈다. 이 경험을 통해, 라이브러리는 이러한 불편함을 어떻게 해결해주는지에 관심이 생겼고, 라이브러리를 하나씩 뜯어보는 과정에서 흥미를 느끼기 시작했다.

데이터 패칭 훅을 만들어보면서 이것을 전역적으로 관리하고 싶다는 생각이 들었고, TanStack Query를 직접 구현해봐야겠다는 생각이 들었다. 처음부터 쉽지 않은 라이브러리를 선택한 건 아닌가 하는 생각도 들었지만, 구현을 하면서 새롭게 알게 된 점들이 많았고, 이러한 지식을 정리하고 공유해보고자 한다.

데이터 캐싱에 대한 호기심

TanStack Query (formerly known as React Query) is often described as the missing data-fetching library for web applications, but in more technical terms, it makes fetching, caching, synchronizing and updating server state in your web applications a breeze.

TanStack Query의 공식 홈페이지 소개 글에서 caching이란 단어가 눈에 들어왔다.

"캐싱은 어떻게 구현하는 걸까?"라는 호기심이 생겼다. 캐싱이란 '데이터를 어딘가에 저장해 두었다가 필요할 때 다시 사용하는 것'이라고 알고 있었지만, 구체적으로 어떤 방식으로, 어디에 저장하는지가 궁금해졌다. 이를 계기로 TanStack Query의 내부 코드를 들여다보기 시작했다.

그 결과, 캐싱이 자바스크립트 객체에 데이터를 저장하는 단순한 방식이라는 사실을 알게 되었고, 그 단순함에 놀랐다. 하지만 단순 저장만으로 끝나는 것이 아니라, staleTime, gcTime 등의 개념을 통해 캐시를 어떻게 효율적으로 관리할 것인가가 TanStack Query의 핵심이라는 것을 깨달았다.

전역 데이터 저장소의 필요성

서버 데이터를 여러 컴포넌트에서 사용할 경우, 상태를 끌어올리는 일이 반복되었다. 이로 인해 props drilling이 발생하고, 이 현상이 심화되면 Context API를 사용할 수밖에 없었다. 서버에서 가져오는 데이터가 많아질 경우 Provider는 늘어나게 되고, 결국 Provider Hell이 발생할 가능성이 높아보였다.
그래서 Provider 없이도 원하는 위치에서 데이터를 사용할 수 있는 전역 데이터 저장소 구조를 설계하고자 했다.

또한, 서버의 데이터를 mutation(수정/추가/삭제)할 때 변경된 데이터를 다시 불러와야 했다. 하지만 이를 위해 refetch 함수를 최상단 컴포넌트에서 props나 Context를 통해 전달해야 했고, 이 과정이 번거롭고 비효율적이라고 느꼈다. 이러한 불편함을 해소하기 위해, 전역 데이터 저장소 구조를 활용하여 mutation 시 자동으로 refetch 될 대상을 명시하는 방식을 구현하고자 했다.

useSyncExternalStore

전역 상태 관리 라이브러리들은 대부분 useSyncExternalStore를 사용하고 있다.

하지만 처음 useSyncExternalStore를 접했을 때는 그 필요성과 내부 동작 방식이 쉽게 와닿지 않았다. 그래서 이 훅을 직접 사용하여 React 외부 상태와의 안정적인 동기화 방식을 직접 구현해보면서, 그 필요성과 작동 원리를 몸으로 이해하고자 했다.

내가 구현을 결심한 이유

당연한 이야기지만, TanStack Query 내부 코드를 보면서 처음에는 코드 대부분이 낯설게 느껴졌다. 대략적인 동작 원리를 파악하는 데에도 상당한 시간이 걸렸다.

코드를 분석하면서 느낀 점은, 제공하는 기능이 많고 강력하긴 하지만 구현 방식이 복잡하게 느껴졌다는 것이다. 그래서 직접 구현해보면서, TanStack Query가 어떤 의도로 이러한 구조를 선택했는지 그 배경을 이해하고자 했다.

구현 기능 목록

주요 기능

  • useQuery
    • Data fetching: isLoading, isError, data 제공
    • Query Key 기반 데이터 캐싱
    • useSyncExternalStore를 이용한 자동 리패칭
  • useMutation: 데이터를 생성, 수정, 삭제하는 등의 작업에 사용되는 훅
    • 리패칭 대상 지정 가능
    • onSuccess / onError 콜백

세부 기능

  • staleTime: Stale-While-Revalidate (SWR) 전략 구현
  • gcTime: Garbage Collection (GC) - 구독이 모두 해제되면 캐시 자동 삭제
  • Request Coalescing: 동일한 데이터에 대한 중복 요청 방지
  • AbortController 기반 요청 취소 (unmount 시 자동 abort)
  • 자동 백그라운드 fetch: 마운트 시 stale 판단 후 자동 fetch
  • 리패칭 조건 트리거
    • refetchOnWindowFocus: 브라우저 포커스 시 자동 fetch
    • refetchOnReconnect: 네트워크 재연결 시 자동 fetch
    • refetchInterval: polling 방식 주기적 refetch
  • refetch(): 수동 리패칭 메서드 제공
  • retryretryDelay: fetch 실패 시 자동 재시도
  • suspense
  • removeQueries, resetQueries로 전체 초기화
  • fetch 중단을 위한 수동 cancel API
  • useInfiniteQuery (무한 스크롤 / 페이지네이션 지원)

추가로 구현할 기능

  • convertFn을 통한 데이터 후처리
  • queryKey 관리하기 쉽게 하는 유틸함수

12개의 댓글

comment-user-thumbnail
2025년 6월 28일

앞으로 올라올 글들이 기대가 되네요!!

1개의 답글
comment-user-thumbnail
2025년 6월 28일

저도 꼭 사용해보고 싶네요~🙏🙏

1개의 답글
comment-user-thumbnail
2025년 6월 28일

🙃🙃🙃

1개의 답글
comment-user-thumbnail
2025년 6월 29일

너무 좋은 글 공유 감사합니다~!

1개의 답글
comment-user-thumbnail
2025년 6월 29일

👻👻👻

1개의 답글
comment-user-thumbnail
2025년 6월 30일

좋은데요~?

1개의 답글