[React] TanStack(React-Query) DeepDive 해보기
FrontEnd/React

[React] TanStack(React-Query) DeepDive 해보기

728x90

 

react-query는 상당히 많이 사용되는 상태관리 라이브러리 중 하나이다. 하지만 어느정도 곁눈길로 사용하는 경우가 많다.(필자 역시)

 

다양한 상태관리 라이브러리

 

 

 

23.10.17 에 react-query가 tabstack으로 새로 릴리즈 되었는데 이부분을 공부할 겸 React-Query가 뭔지 한번 깊게 공부해 보려 한다.

 

TanStack(REACT-QUERY)가 뭔데?!!

먼저, TanStack의 공식 홈페이지에서는 해당 라이브러리를 아래와 같이 소개하고 있다.

TanStack Query (FKA 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(FKA React Query)는 종종 웹 애플리케이션용 누락된 데이터 가져오기 라이브러리로 설명되지만 좀 더 기술적인 용어로 말하면 웹 애플리케이션에서  서버 상태 가져오기, 캐싱, 동기화 및 업데이트를 매우 쉽게 만듭니다.

 

 

 

 

2021년도에 카카오페이 기술블로그에 올라온 글을 보면 React-Query를 아래와 같이 설명하고 있다.

🙌 「if(kakao)2021 - 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유」 세줄요약 🤟

React Query는 React Application에서 서버 상태를 불러오고, 캐싱하며, 지속적으로 동기화하고 업데이트하는 작업을 도와주는 라이브러리입니다.복잡하고 장황한 코드가 필요한 다른 데이터 불러오기 방식과 달리 React Component 내부에서 간단하고 직관적으로 API를 사용할 수 있습니다.더 나아가 React Query에서 제공하는 캐싱, Window Focus Refetching 등 다양한 기능을 활용하여 API 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.

https://tech.kakaopay.com/post/react-query-1/

 

카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유 | 카카오페이 기술 블로그

카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유에 대해 설명합니다. 이 글은 연작 중 1편에 해당합니다. 1편: 카카오페이 프론트엔드 개발자들이 React Query를 선택한 이유, 2편: React Que

tech.kakaopay.com

 

 

 

요약하자면.. API요청과 관련된 값을 불러오고, 캐싱하고, 시간이 지난 데이터들을 업데이트하는 것을 담당하는 라이브러리라고 생각할 수 있다.

 

 

Redux의 한계

 

Redux는 지금까지도 인기가 많고 상당히 많은 곳에서 사용되는 라이브러리이다. 현재는 덜해졌지만 과거에는 Redux가 React의 기본스택으로 느껴질 정도였다.

 

하지만 Redux를 활용해서 다양한 API 데이터 (비동기 데이터)들을 관리하는것은 쉽지 않았다. 캐시 최적화를 수행하기 쉽지 않거나 복잡한 사용자 시나리오에 대응하는것은 쉽지않았고 redux-saga와 같은 미들웨어를 사용해야 했다. 부가적으로는 Redux를 사용하는데는 코드가 너무 많이 작성되어야 한다는 단점도 있었다.

 

어쩌면 당연한 일이다. Redux는 API통신 및 비동기 데이터를 관리하는 라이브러리가 아니기 때문이다.

 

앞에서 말한 redux-saga와 같은 미들웨어를 통해서 기능 구현 자체는 가능하지만 해당 부분들을 개발자가 모두 작성해야 하고 로직이 복잡해진다면 코드가 엉킬 가능성도 커진다.

 

 

 

TanStack 사용

 

TanStack Query를 사용하면  이와같은 문제를 쉽게 해결할 수 있다.

 

아래는 TanStack에서 제공하는 github프로젝트 통계를 가져와보는 예제이다.

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isPending, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then((res) =>
        res.json(),
      ),
  })

  if (isPending) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>✨ {data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}

 

실행결과

 

 

 

 

실행을 해보면 위와 같이 api요청을 통해 가져온 값을 가져온다.

 

재미있는점은 api요청이 진행중인 경우 Loading... 이라는 문구를 출력한다.

 

 

 

 

 

const { isPending, error, data, isFetching } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      axios
        .get('https://api.github.com/repos/tannerlinsley/react-query')
        .then((res) => res.data),
  })

 

 

보면 알겠지만 queryFn과 queryKey를 받는다.

 

queryKey : 응답데이터의 unique Key로 데이터를 캐싱할때 사용한다.

queryKey는 안전하게 해시로 변경되며, 이 queryKey가 바뀌는 경우 쿼리가 업데이트 된다.

 

queryFn : Promise를 반환하는 함수로 API요청이 들어간다.

 

 

useQuery의 리턴 값은 굉장히 다양하게 나온다.

 

진짜 많다

 

https://tanstack.com/query/latest/docs/framework/react/reference/useQuery

 

useQuery | TanStack Query Docs

 

tanstack.com

 

조금 중요한 것들만 살펴보자.

 

 

data: TData
-> 마지막으로 받은 데이터
error: null | TError
-> 오류가 발생한 경우 쿼리에 대한 오류 객체가 들어있음
isPending: boolean
-> 쿼리가 완료되었는지 아닌지에 대한 정보가 들어있음
isSuccess: boolean
-> 쿼리가 오류없이 데이터를 받고 데이터를 표시할 준비가 되어있는지 여부
isLoading: boolean
-> isFetching && isPending과 같은 효과로 로딩중을 표시

 

 

 

 

 

useQuery 원리

그렇다면 useQuery는 어떤 원리로 작동할까?

 

useQuery가 비동기 상태관리를 전문적으로 다루는 훅이긴 하지만 결국 queryKey를 바탕으로 queryFn을 실행하기때문에 아래와 같은 로직을 가지고 있을 것이다.

 

( 아래는 내가 임의로 useQuery를 흉내낸 것으로 실제 구현과는 차이가 있다)

 

import { useState, useEffect } from "react";

function useMyQuery(queryKey, queryFn) {
  const [isLoading, setIsLoading] = useState(false);
  const [data, setData] = useState(null);
  const [isError, setIsError] = useState(false);

  useEffect(() => {
    const fetchData = async () => {
      try {
        setIsLoading(true);
        const result = await queryFn(queryKey);
        setData(result);
        setIsError(false);
      } catch (error) {
        setIsError(true);
      } finally {
        setIsLoading(false);
      }
    };

    fetchData();
  }, [queryKey, queryFn]);

  return { isLoading, data, isError };
}

 

 

 

코드를 보면 useQuery는 단순히 비동기 데이터의 값만을 관리한다는 점을 제외하면 일반적인 상태관리 라이브러리와 크게 다르지 않음을 알 수 있다.

 

 

즉, TanStack은 전역상태 라이브러리로도 사용할 수 있기 때문에 아래와 같이 Provider로 감싸줘야 한다.

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

 

 

 

만약 감싸줬다면 아래와 같이 react-query를 전역상태 라이브러리처럼 사용하는 것 또한 가능하다.

// index.tsx
const queryClient = new QueryClient();
const reqHeaders = getRequestHeader(headers);
 
queryClient.setQueryData(headersQueryKey, () => reqHeaders);
 
// child.tsx
const queryClient = new QueryClient();
 
const headers = queryClient.getQueryData<ReqHeaders>(headersQueryKey);

 

 

 

 

 

useMutation

react-query를 통해 서버에 데이터를 요청할 때 사용된다 (POST 등)

 

useQuery가 데이터를 조회하는데 사용된다면 해당 함수는 삽입, 업데이트, 삭제 등의 요청을 하는데 사용된다. 일반적으로는 아래와 같이 사용할 수 있다.

 

// 1
const savePerson = useMutation((person: Iperson) => axios.post('/savePerson', person));

// 2
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person)
})

 

 

데이터를 요청하자마자 데이터를 갱신하고 싶다면 아래와 같이 queryClient.invalidateQueries를 활용하여 현재 쿼리를 즉시 삭제하고 다시 패칭해오는 작업을 수행하게 할 수도 있다.

 

const queryClient = useQueryClient()
  return useMutation((person: Iperson) => axios.post('/savePerson', person), {
    onSuccess: () => {
      queryClient.invalidateQueries('get-product')
    },
  })

 

 

 

결국 useQuery와 useMutation은 크게 다르지 않다. 오히려 더 단순한 형태이다.

import { useState, useCallback } from "react";

function useMyMutation(mutationFn) {
  const [isLoading, setIsLoading] = useState(false);
  const [data, setData] = useState(null);
  const [isError, setIsError] = useState(false);
  const [error, setError] = useState(null);

  const executeMutation = useCallback(async (variables) => {
    setIsLoading(true);
    setIsError(false);
    setError(null);
    try {
      const result = await mutationFn(variables);
      setData(result);
    } catch (error) {
      setIsError(true);
      setError(error);
    } finally {
      setIsLoading(false);
    }
  }, [mutationFn]);

  return { isLoading, data, isError, error, executeMutation };
}

 

 

 

 

 

React-Query -> TanStack-Query 변경점

 

그렇다면 TanStack으로 이름이 바뀌면서 어떤 점들이 바뀌었을까?

 

1. 하나의 객체로 쿼리옵션 관리

 

원래 react-query는 아래와 같은 방식으로 선언할 수 있었다.

useQuery(queryKey, queryFn, options);
useQuery(queryKey, options); // default query function 사용할 경우 query function 생략 가능
useQuery(options);
// v5 이전에는 queryKey만 필수 옵션

 

 

근데 해당 인자를 넘기는 방식이 아래와 같이 단일 객체를 넘기는 식으로 변경되어 통일성을 보장하고 인수 순서에 따른 혼란이 없게 만들었다고 한다.

- useQuery(key, fn, options)
+ useQuery({ queryKey, queryFn, ...options })

 

 

 

2. onSuccess, onError, onSettled 콜백 삭제

 

라이브러리 팀에서 제공한 해당 메서드들을 삭제한 이유는 아래와 같다.

 

예측 가능하고 일관성있는 useQuery
상태 동기화를 목적으로 사용했을 때 발생하는 추가 렌더 사이클. 예) onSuccess 콜백에 로컬 또는 전역 상태 업데이트 (참고)
콜백이 호출되지 않을 여지 예) staleTime 설정으로 query function이 호출되지 않아 의도한 콜백이 실행하지 않을 경우(참고)

 

 

참고로 Mutation의 콜백은 그대로 남아있다고 한다.

 

 

 

3. Suspense를 지원

리액트 18의 Suspense를 지원하는 useSuspenseQuery, useSuspenseInfiniteQuery, useSuspenseQueries 가 추가되었다.

 

const { data: post } = useSuspenseQuery({
  // const post: Post
  queryKey: ['post', postId],
  queryFn: () => fetchPost(postId),
})

 

 

 

4. useMutationState로 mutation 상태 공유

mutationState로 MutationCache안의 상태를 공유하는 것이 가능해진다. filters옵션으로 mutation을 필터링 하고 select옵션으로 해당 값을 가공하는 것 또한 가능하다.

// 모든 variables 
const variables = useMutationState({
  filters: { status: 'pending' },
  select: (mutation) => mutation.state.variables,
})

 

 

// mutationKey로 mutation 식별
const mutationKey = ['posts']
const mutation = useMutation({
  mutationKey,
  mutationFn: (newPost) => {
    return axios.post('/posts', newPost)
  },
})
const data = useMutationState({
  filters: { mutationKey },
  select: (mutation) => mutation.state.data,
})

 

 

 

5. cacheTime -> gcTime

cacheTime은 쿼리를 사용하는 컴포넌트가 언마운트 되면서 쿼리 인스턴스가 비활성화됐을 때 부터 유효한 시간입니다. 따라서 데이터가 캐싱돼있는 시간보다는 가비지 컬렉팅 대상이 되기까지의 시간이 더 적합한 설명입니다.

 

위 이유로 이름을 바꿨다고 한다.

 

6. isLoading -> isPending

 

status의 loading은 pending으로 변경됩니다.
isLoading은 isPending으로 변경됩니다.
isPending && isFetching의 기능인 isInitialLoading은 isLoading으로 변경됩니다.

 

좀더 직관적인 상태표현을 위해 이름이 바뀌었다!!

 

 

 

이외에도 타입관련 수정, 리액트 18과의 연동성 등등 많은 변화가 이루어졌다. 기타 변화들은 아래에서 더 봐도 괜찮을 것 같다.

 

https://tanstack.com/query/v5/docs/framework/react/guides/migrating-to-v5

 

Migrating to TanStack Query v5 | TanStack Query Docs

 

tanstack.com

 

역시 공식문서들이 최고다.

728x90