JULOG

写.真

back
한국어日本語

Tanstack query 왜 사용하는거야 ?

2024년 08월 25일

Tanstack-query

⚠️ 공부한 내용을 멋대로 기록한 것입니다. 작성된 정보는 순 엉터리정보일 수 있으니 참고하실 때 조심하세요.

상태관리를 클라이언트 상태관리와 서버 상태관리를 분리하여 관리하는 프로젝트가 많이 보이고 있다.

전역 상태관리는 우리가 흔히 알고있는 전역 스토어에 관리하는 방법이다.

Redux, Recoil, Zustand, MobX은 익히 들어본 라이브러리일 것이다.

그러면 상태는 상태인데 서버 상태라는 것은 무엇일까 ?

그리고 왜 서버 상태를 따로 관리하는 것일까?


서버상태 ?

  • 기본적으로 서버로부터 가져오는 데이터 ( API요청을 통해 가져온 사용자 정보, 게시물 목록 등)
  • 서버상태는 외부 데이터이기 때문에 항상 일관성이 보장되지 않으며, 갱신이 필요할 때 서버와 통신이 필수적

전역상태 ?

  • 클라이언트 측 애플리케이션에서 직접 관리되는 상태 ( 인터페이스의 상태, 로그인 여부, 페이지 간 공유 상태 등 )
  • 외부 서버와 무관하게 조작 가능

기존에는 서버에서 데이터를 불러오면 state를 가공하여 상태를 저장하는 방식으로 사용하였다. redux, redux-saga를 사용하는 방식도 이러하다.

store에 api fetch 코드가 점점 늘어나면서 코드의 복잡도가 올라간다. 즉 상태 관리의 복잡성이 올라가게 된 것이다. 상태관리가 복잡하면 얼마나 복잡해질 수 있겠는가? 라는 의문이 들지만(큰 프로젝트 참여하지 못해본 나) 규모가 작은 프로젝트에서 전역 상태와 서버 상태를 분리해서 관리해본 경험으로써 이제는 tanstack query 없는 미래는 상상이 안될 정도이다.

그런데 내가 진짜로 tanstack query를 제대로 쓰고 있는지 정리하면서 확인해보려고 한다.


Tanstack query가 뭐야 ?

Tanstack query ? react-query v4부터 우리 이제 react에 종속된 라이브러리가 아니라 범용적으로 사용할 수 있는 라이브러리로 확장하자 ! 라는 것으로 Tanner Linsley의 이름을 따서 만들어졌다. 항상 Tanstack이 뭐야? 무슨 뜻이지 ? 라는 의문을 품었는데 결국 오픈소스 창시자 이름에서 따온거다. 공식문서에 TanStack Query, TanStack Table, TanStack Router 등 탠스택 머시기 많은 이름이 나오지만 아직까지 사용해보진 않았다.

이상 TMI였다.

Tanstack query는 서버에서 받아온 데이터 관리를 효율적으로 도와주는 라이브러리이다.

공식문서에 작성된 Tanstack query의 기능들을 나열해 보겠다.

  • 캐싱
  • 동일한 데이터에 대한 여러 요청을 하나의 요청으로 중복 제거
  • 백그라운드에서 “out of date” 데이터 업데이트
  • 데이터가 언제 “out of date”인지 판단
  • 데이터 업데이트를 가능한 한 빨리 반영
  • 페이지네이션과 지연 로딩과 같은 성능 최적화
  • 서버 상태의 메모리 관리 및 가비지 컬렉션
  • 구조적 공유를 통한 쿼리 결과 메모이제이션

정말 다양한 기능들을 지원한다. 그 중 가장 편리하고 효율적이라 느끼는 것은 캐싱관련.

놀라운건 이러한 기능을 라이브러리를 설치하고 딸깍하나로 가능하다는 것인데, 이러한 기능들을 전부 라이브러리 없이 구현하려면 손이 꽤 많이 갈 것이다. ( 구현 가능할지도 모르겠는 수준이지만 )


그래서 사용하는 이유

새로운 프로젝트에서 도입하지 않을 이유가 없다고 생각한다.( 내 생각 ) 물론 모든 도구는 동작 방식을 철저히 이해하고 원리를 알고 사용하는 것이 맞다. 사실 나의 경우는 tanstack query를 사용하게된 계기가

tanstack query가 요즘 좋다던데 ? → 나도 사용해봐야지 → 프로젝트에 적용 → 왕 개쩐다

이런 프로세스로 처음 사용해보게 되었다. 공식문서를 보며 어찌어찌 사용하고 있는데 정확한 동작 방식에 대한 이해가 부족한건 사실이다. 블로그 포스팅으로 이 주제를 다루는 이유이다. ㅎㅎ..

그래서 왜 사용하냐구여 ?.?

많은 기능들이 있지만 사실 체감하는 것은 캐싱 기능이다. 프론트엔드가 고려할 것들이 무진장 많지만 그 중 하나는 서버 리소스를 최소화하는 방법에 대해 프엔 개발자들은 많은 고민을 한다. 다들 프엔이라면 UI아키텍쳐를 짤 때 어떻게 서버로의 요청을 줄일 수 있을까라는 고민을 한 번쯤 해봤을 것이다. 우리의 고민들을 이쁜 이 라이브러리가 많은 부분을 해결해준다. 탠쿼리(Tanstack query)를 한 번이라도 써 본 사람이라면 극히 공감갈 것이다. ( 공감해줘 )

내 기준으로 사용하는 이유 두 번째는 서버 데이터를 가시화할 수 있다는 점이다.

dev mode를 사용하면 데이터가 어떤 상태(stale, inactive, fresh… etc)인지 모두 표시가 된다. 이 부분에서는 staleTime, gcTime에 대한 어느정도 학습이 필요하겠지만, 구글에 많은 분들이 정리해주셨기 때문에 이해는 쉽게 할 수 있었다.

내 기준으로 사용하는 세 번째 이유는 상태 관리의 복잡성 최소화이다.

이전에는 전역 store에서 클라이언트 상태 + 서버 상태를 모두 관리하는 방식을 사용하였지만, 한 번 분리해봤더니(?) 실질적으로 클라이언트에서 전역으로 관리하는 데이터는 많지 않았고, 프로젝트의 전체적인 구조를 보는데 더욱 수월해졌다.

이상 내가 정해본 탠스택쿼리를 사용하는 이유를 나열해봤다.

그러면 실제로 어떤 기능을 사용했는지 정리 ㄱㄱ


실제로 사용해본 기능

이 라이브러리를 사용한다고 해서 모든 기능을 사용해본 것은 아니다. 대부분의 기능들은 실제로 기능 구현을 할 때 이러이러한 기능이 필요한데 옵션에 있을까 ? 하고 공식문서를 기웃기웃 찾아봐서 그 기능을 적용시키는 방법이다. 그러면 이러이러한 기능이 필요한데와 옵션에 있을까? 하는 옵션을 정리해보겠다 !

1. refetch, enabled: boolean

기본적으로 상세 영수증을 클릭할 때에만 데이터를 받아오고 싶은데, Query 기본 옵션으로는 해당 컴포넌트가 렌더되었을 때 Query 실행되는 값이 디폴트이다. 어떠한 행동 또는 값이 있을 때 Query를 받아오고 싶으면 Query옵션으로 refetchenabled 옵션을 사용하여 이벤트를 제어할 수 있다. enabled에 false를 주면 컴포넌트가 렌더링될 때 데이터를 받아오지 못하므로 refetch 메서드를 통해 데이터를 수동으로 다시 요청해야 한다.

주의할 점은, enabled: false로 설정하면 queryClient가 쿼리를 다시 가져오는 방법 중 invalidateQuries와 refetchQuries를 무시한다.

1// serial이 있을 시 Query를 실행시키는 Query Custom Hook이다. 2// serail이 truthy, falsy 값 판단 3// false를 주면 쿼리가 자동 실행되지 않는다. 4 5export const useDeviceStateQuery = (serial: string) => { 6 return useQuery({ 7 queryKey: ['deviceState', serial], 8 queryFn: () => deviceApi.fetchDevice(serial), 9 enabled: !!serial, 10 retry: false, 11 }); 12};

2. retry : (boolean | number | (failureCount: number, error: TError) => boolean)

기본적으로 쿼리요청을 클라이언트 환경에서는 3번, 서버 환경에서는 0번 수행한다.

이것을 조절하기 위해서 retry옵션에 불리언 값 또는 숫자 값을 주어 특정 횟수만큼 재요청하는 옵션이다.

값으로 숫자를 넣을 경우, 실패한 쿼리가 해당 숫자를 충족할 때까지 요청을 재시도한다.

3. select :(data: TData) ⇒ unknown

반환된 데이터를 일부를 변환하거나 선택할 수 있다.

참고로 반환된 데이터 값에는 영향을 주지만 쿼리 캐시에 저장되는 내용에는 영향을 전혀 주지 않는다.

useInfiniteQuery에서 Data를 평탄화 시켜주기 위해 사용해본 적이 있다.

서버에서 받아오는 데이터를 미리 정제할 수 있기에 option 중 많이 사용하는 것 같다.

4. useQuries

유저ID를 통해 유저 정보를 받을 때 영수증 내의 유저ID 받아서 해당 유저 ID를 파라미터로 넣어 개별 유저정보를 받아와야하는 경우가 생겼다. 비동기 요청을 id 개수만큼 돌려야하는 것이다. userId를 배열로 저장하여 매핑시켜줄 수 있지만 tanstack query가 지원해주지 않을까 또 공식문서를 기웃기웃거리다 발견하였다.

영수증에 있는 userId를 받아오고 new Set()을 통해 중복을 제거한 유저ID 배열을 만들어 useQuries를 통해 id 갯수만큼 데이터를 받아왔다.

1 const uniqueUserIdsArray = storeData?.usages.items.map(data => data.userId); 2 const userSet = new Set(uniqueUserIdsArray); 3 const uniqueUsers = [...userSet]; 4 const usersQuery = useUserQuery(uniqueUsers); 5 6export const useUserQuery = (userIds: number[]) => { 7 return useQueries({ 8 queries: userIds.map(userId => ({ 9 queryKey: ['storeUser', userId], 10 queryFn: () => receiptApi.fetchStoreUser(userId), 11 suspense: true, 12 })), 13 }); 14};

Issue : 옵션으로 아무것도 주지않고 받아온 데이터를 사용하려고 하니 에러가 발생하였다.. 사실 어떤 에러인지 기억안남…. 아마 데이터 페칭 중 해당 데이터를 참조해서 나는 에러로 기억이 난다. 구글링 삽질을 통해 원인을 알아채고 suspense: true 옵션을 주어 데이터가 모두 로드된 후에 참조할 수 있게하여 에러를 해결한 경험이 있다. 근데 다른 프로젝트에서 useQuries를 사용할 일이 있었는데 같은 이슈로 삽질한 경험이 있어 현타가 온 적이 있었다. 이상 또 tmi useQuries 훅의 데이터 반환값은 모든 쿼리 결과가 포함된 배열을 반환한다. 그래서 반환값을 가공해줘야하는데 combine이라는 옵션을 통해 제공한다. combine은 모든 쿼리 결과가 포함된 배열을 단일 값으로 결합할 수 있다. 하지만 나는 이 것을 방금 알았고 다음주 출근 후 코드를 수정하려고 한다.

5. useQueryClient

아주 많이 쓰이는 녀석이다. useQueryClient훅은 queryClient를 반환하는데 가장 기본적으로 defaultOption을 통해 모든 query, mutation에 대한 기본값을 정의한다. 그리고 또 가장 많이 사용하는 것은 invalidateQueries이다. 캐시 데이터를 최신화할 때 많이 사용하는 함수이다. 단일 또는 여러 쿼리를 무효화하고, 다시 값을 가져오는데 많이 사용된다. 어떤 쿼리를 무효화할 것인가 판별하는 것은 queryKey를 통해 판별한다. 어쩔 때는 쿼리 무효화가 안될 때가 있는데 그 때는 queryKey를 올바르게 설정하였는지 확인해보자. 간간히 queryKey의 값들을 모두 입력안했을 시 발생한다. useQueryClient에서 많은 것을 제공하지만 아직 나의 학습이슈로 tanstack query 2탄( 방금 만듦)에서 다루어 보겠다 !

6. useInfiniteQuery

실제로 작업을 하다보면 무한스크롤을 구현할 경우가 많이 생긴다. useQuery를 통해 구현할 수도 있지만 이 훅을 사용하면 많은, 수많은, 수수많은 것들을 제공하여 한 번 학습하고 나면 유용하게 써먹을 수 있는 훅이다. 근데 난 역시 머리가 그렇게 좋지 않은 것 같다. 이 훅에서 기본적으로 설정해야되는 설정을 이해하는데 시간이 오래 걸렸다. 사실 솔직히 말하면 아직도 헷갈린다. getNextPageParam에서 인자로 lastPage, allPages 등등 실제로는 내가 설정해야되는 값들을 자동적으로 받을 수 있으니 이게 된다고? 하면서 사용했던 기억이 있다. 마치 매직처럼 모두 다 알아서 해주고 curosr 방식, page 방식의 혼동 때문에 이해하기 더 어려웠던 것 같다. 이 훅과 함께 react-intersection-observer를 같이 사용하였다. 이 훅도 학습 후 2탄에서 정리해보겠다. (미루기)

7. Optimistic Update

낙관적 업데이트라고 하는데 이름이 생소해서 윙? 했지만 설명을 보고 이해했다. 미리 업데이트한다는 뜻이다. 서버에서 받아오는 데이터로 UI를 반영하면 네트워크 환경이 좋지 못하거나 서버가 느릴 때는 사용자 경험이 좋지 않을 수 있다. 이벤트가 일어나서 서버에게 바뀐 값을 반영해주기 전에 어차피 업데이트할 것이라고 가정하여 미리 UI를 업데이트 시켜주고 서버를 통해 검증받고 업데이트 또는 롤백하는 형식이다. 그래서 onMutate, onError, onSettled를 주어, 요청 했을 때, 실패 했을 때(롤백), 오류 또는 성공 후 행동을 지정해줘야 한다. 예를 들면 좋아요 기능같은 경우 state를 바꾸면 즉시 업데이트 되지만 그 state값이 서버 데이터에 의존하면 값이 늦게 바뀔 수 있어 낙관적 업데이트를 사용한다. 나도 실제로 체크박스 상태를 보여줘야하는데 딸깍하면서 늦게 반영돼서 사용한 적이 있지만, 지금은 기존의 로직이 바뀌어 이 기능을 사용하지 않고 있다.

8. useQueryErrorResetBoundary

ErrorBoundary와 useQueryErrorResetBoundary를 결합해 선언적으로 에러가 발생했을 때 Fallback UI를 보여줄 수 있다. ErrorBoundary를 통해 에러가 발생 했을 시 상위 컴포넌트에서 그 에러를 감지하여 에러를 처리할 수 있게 도와준다. 여기서 내부에서 useQueryErrorResetBoundary 를 호출하여 reset함수를 가져온다. reset함수를 통해 모든 쿼리 에러를 초기화할 수 있다. 이렇게 내가 실제로 사용해본 기능들을 간단하게 나열해 보았다. 복기하는 차원에서 작성한 느낌도 있다. tanstack query 공식문서는 이상하게 보기 어렵고 보기 싫게 되어 있다. 나만 그런가….. ? 그래서 한국인들이 공식문서를 잘 정리한 곳이 있는데 tanstack query를 사용하는 한국인이라면 따봉하나 박아주자… 사실 아래의 url이 내가 작성한 글 중 제일 실속있을지 모른다… https://github.com/ssi02014/react-query-tutorial?tab=readme-ov-file#enabled-refetch




사용했을 때의 편리함.. 체감중?

아직 사용못해본 기능들과 옵션들이 많지만 필요하지 않은 서비스 로직에 일부로 꾸역꾸역 사용하는 것도 웃겨서 2탄은 시간이 지나고 올릴 것 같다. 절~대 미루는게 아니다. ㅎㅎ 사용했을 때의 편리함 ? 개편리하다… 사실 편리함을 느끼려면 먼저 불편함이 무엇인지 알아야하는데 대충 안겪어봐도 알 것같다. 이러한 라이브러리만 사용해서 실제 기본적인 것들을 놓칠 수 있다고는 말하지만 개발할 날은 아직 많이 남아있다. 이런 것도 써보고 직접 구현도 해보고 하는거지라는 마인드라. 하나라도 많이 아는게 중요한 것이 아닌가? 그래서 tanstack query는 계속 사용할 것이냐 물어보면 무조건 사용한다라는 입장이다. 이상 tanstack query 1편 끝… (글을 쓰면서 1편,2편이 갑자기 생겨버렸다…)