본문 바로가기
매일 해내는 개발/React

[React] 서버 상태 관리 라이브러리 react-query의 개념과 useQuery, useMutation

by 해야지 2023. 2. 19.
반응형

react-query

  • 개념
    1. react-query란?
      • 데이터 Fetching, 캐싱, 동기화, 서버 쪽 데이터 업데이트 등을 쉽게 만들어 주는 서버 상태 관리 라이브러리
    2. react-query를 사용하는 이유?
      • 서버 데이터만 따로 관리하기 위해서!
        • 기존의 redux와 같은 라이브러리들은 클라이언트 쪽 데이터를 관리하기 적합했지만 서버 쪽 관리에는 적합하지 않는 점들이 있었다.
        • 기존에는 서버의 데이터와 클라이언트의 데이터가 공존하게 되고 그 데이터가 서로 상호작용하면서 섞여버린다. (예, 서버에는 이미 패치된 데이터가 저장되어 있고 클라이언트에는 패치되기 전 데이터가 유저에게 사용되고 있음)
        • 기존 라이브러리들은 미들웨어를 통해 서버 데이트 관리를 할 수는 있지만 보일러 플레이트가 많아서 코드양이 많아지고 유지보수가 어려워진다.
    3. React Query에서의 Server State 정의
      • Client가 제어하거나 소유하지 않는 위치에서 원격으로 유지 됨.
      • fetching 및 updating을 위한 비동기 API를 필요로 함.
      • 상태가 공유되며 사용자 모르게 변경될 수 있음.
      • 주의하지 않으면 애플리케이션이 잠재적으로 "out of date" 상태가 될 수 있음.
    4. 특징
      • Caching 지원.
      • 동일한 데이터에 대한 중복 요청을 제거하고 한 번만 요청하도록 함.
      • "out of date" 상태의 데이터를 파악하고 updating 지원.
      • Pagination 및 Lazy Loading 성능 최적화.
      • Server State의 메모리 관리 및 garbage collection 지원.
      • React Hooks와 유사한 인터페이스 제공.
    5. Server State 모델링 
      • Fetching: 초기 상태이며 백엔드와 같은 외부 소스로부터 데이터를 가져오기 위해 동작
      • Fresh: Fetching 이후에 Server-side와 Client-side의 데이터가 동일하게 유지되는 상태
      • Stale: 데이터가 오래된 상태이며 Fetching을 통해 Fresh 상태로 유지해줘야 함
      • Inactive: 애플리케이션에서 사용되지 않는 데이터에 대한 상태로 React Query에서 브라우저 캐시를 관리하는 가비지 컬렉터에 의해 삭제 됨
      • Deleted: Inactive 상태의 데이터가 캐시에서 삭제된 상태

useQuery

서버로부터 데이터를 조회할 때 사용한다.

사용방법

const {data, error, isError, isSuccess} = useQuery(queryKey, queryFn) 
또는
const {data, error, isError, isSuccess} = useQuery({queryKey: queryKey, queryFn: queryFn}) 
  • 전체옵션
const {
  data,
  dataUpdatedAt,
  error,
  errorUpdatedAt,
  failureCount,
  failureReason,
  isError,
  isFetched,
  isFetchedAfterMount,
  isFetching,
  isPaused,
  isLoading, //데이터가 없고 fetching하는 상태
  isLoadingError,
  isPlaceholderData,
  isPreviousData,
  isRefetchError,
  isRefetching,
  isInitialLoading,
  isStale,
  isSuccess,
  refetch,
  remove,
  status,
  fetchStatus,
} = useQuery({
  queryKey,
  queryFn,
  cacheTime,
  enabled,
  networkMode,
  initialData,
  initialDataUpdatedAt,
  keepPreviousData,
  meta,
  notifyOnChangeProps,
  onError,
  onSettled,
  onSuccess,
  placeholderData,
  queryKeyHashFn,
  refetchInterval,
  refetchIntervalInBackground,
  refetchOnMount,
  refetchOnReconnect,
  refetchOnWindowFocus,
  retry,
  retryOnMount,
  retryDelay,
  select,
  staleTime,
  structuralSharing,
  suspense,
  useErrorBoundary,
})

queryKey

  • useQuery마다 부여되는 고유 Key 값으로 단순하게 문자열로 사용되기도 하고 배열의 형태로 사용될 수도 있다.
// 문자열 => 자동으로 길이가 1인 배열로 인식
const res = useQuery('persons', queryFn);

// 배열1
const res = useQuery(['persons'], queryFn);

// 배열2
const res = useQuery(['persons', 'add Id'], queryFn);

// 배열3 => 배열2와는 다른 Key
const res = useQuery(['add Id', 'persons'], queryFn);

// 배열4
const res = useQuery(['persons', {type: 'add', name: 'Id'}], queryFn);
  • queryKey는 react-query가 query 캐싱을 관리할 수 있도록 도와준다.
const getPersons1 = () => {
        const res1 = useQuery(['persons'], queryFn1);
    }

    const getPersons2 = () => {
        const res2 = useQuery(['persons'], queryFn2);
    }
  • res1과 res2에서 queryFn이 달라도 같은 queryKey로 요청했기 때문에 서버에 1개의 request만 전달이 된다. (일반적인 데이터 요청이라면 두개의 요청이 갈 것)따라서 해당 코드는 queryFn에 관계 없이 res1에서 요청했던 결과를 그대로 res2에 저장하는 꼴이 된다.
  • ⇒ res1에서 서버에 요청하게 되면 res2에서는 이미 동일한 queryKey에 대한 결괏값이 있기 때문에 추가 요청 하지 않고 res1의 결과를 그대로 사용한다.
  • const getPersons1 = () => { const res1 = useQuery(['persons'], queryFn1); } const getPersons2 = () => { const res2 = useQuery(['persons'], queryFn2); }

queryFn

promise 처리가 이루어지는 함수. 즉 axios와 같이 서버에 API를 요청하는 코드

staleTime

  • default value = 0
  • 데이터가 fresh → stale 상태로 변경되는데 걸리는 시간
  • fresh 상태일때는 쿼리 인스턴스가 새롭게 mount 되어도 네트워크 fetch가 일어나지 않는다.
  • 데이터가 한번 fetch 되고 나서 staleTime이 지나지 않았다면 unmount 후 mount 되어도 fetch가 일어나지 않는다.

staleTime = 0 이면 react query는 서버로부터 받은 데이터가 오자마자 fresh 하지 않다고 생각해서

화면 전환이나 탭 전환 등만 시도해도 서버로부터 fresh한 데이터를 전달 받기 위해 계속해서 refetch를 한다.

*이는 fresh한 데이터를 계속 업데이트 받을 수 있지만 서버에 부하를 줄 수 있다고 생각한다.

cacheTime

defaultValue = 5(분)

staleTime과 유사한 역할을 수행한다. 말 그대로 캐싱 처리가 이루어지는 시간이다.

따라서 queryKey에 매핑되는 데이터가 사용되지 않는 시점을 기준으로 5분이 지나지 않으면

해당 queryKey를 다시 호출할 때 이전에 가져왔던 데이터를 그대로 보여주게 된다.

하지만 5분이 지나면 캐시 가비지 콜렉터 타이머가 실행되며 기존 데이터는 삭제 처리가 되고 queryKey를 재호출 후 서버에 데이터를 재 요청하게 된다.

따라서 useQuery는 staleTime과 cahcheTime 둘 다 만족하지 않으면 서버에 다시 데이터를 요청하게 된다.

const res = useQuery({
    queryKey: ['persons'],
    queryFn: () => axios.get('http://localhost:8080/persons'),
    staleTime: 5000, // 5초
    cacheTime: Infinity // 제한 없음
});

QueryClientProvider

리액트 애플리케이션에서 비동기 요청을 처리하기 위한 Context Provider로 동작하며 하위 컴포넌트에서 QueryClient를 사용할 수 있게 해준다.

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
);

refetchOnWindowFocus

staleTime이 지나면 단순히 window focus의 전환(사용자가 사용하는 윈도우의 포커스가 다른 곳을 갔다가 다시 화면으로 돌아오기)만으로도 refetch가 이루어지는데 이는 refetchOnWindownFocus의 기본 값이 true이기 때문이다.

이 기능이 필요없다면 false로 변경하면 된다.

refetchOnWindowFocus=false이면 staleTime이 지나고 다른 페이지로 이동되었다가 다시 현재 화면으로 돌아오는 케이스에는 refetch가 이루어진다.

  • 전역 설정하는 방법: QueryClient 생성시 설정
  • const queryClient = new QueryClient( { defaultOptions: { queries: { refetchOnWindowFocus: false, // window focus 설정 } } } ); // queryClient 생성
  • useQuery마다 설정하는 방법
  • const res = useQuery({ queryKey: ['persons'], queryFn: () => axios.get('<http://localhost:8080/persons>'), refetchOnWindowFocus: false // window focus 설정 });

QueryCache

QueryCache는 React Query를 이용하여 사용된 쿼리의 메타 정보와 상태 등의 데이터를 저장하는 용도로 사용한다. 또, onError, onSuccess 콜백을 사용하여 애플리케이션 전역에서 이벤트를 핸들링 할 수 있다.

Normally, you will not interact with the QueryCache directly and instead use the QueryClient for a specific cache.

  • 쿼리 클라이언트에서 설정 가능
 

QueryCache | TanStack Query Docs

The QueryCache is the storage mechanism for TanStack Query. It stores all the data, meta information and state of queries it contains. Normally, you will not interact with the QueryCache directly and instead use the QueryClient for a specific cache.

tanstack.com

useMutation

데이터 조회 이외의 crerate, update, delete 등의 변경 작업을 할 때는 useMutation을 사용한다.

사용방법

const savePerson = useMutation(mutationFn);
또는 
const savePerson = useMutation({mutationFn: mutationFn})
  • 전체 옵션
const {
  data,
  error,
  isError,
  isIdle,
  isLoading,
  isPaused,
  isSuccess,
  failureCount,
  failureReason,
  mutate,
  mutateAsync,
  reset,
  status,
} = useMutation({
  mutationFn,
  cacheTime,
  mutationKey,
  networkMode,
  onError,
  onMutate,
  onSettled,
  onSuccess,
  retry,
  retryDelay,
  useErrorBoundary,
  meta
})
mutate(variables, {
  onError,
  onSettled,
  onSuccess,
})

mutationFn

수정, 삭제 등의 API를 요청하는 함수

mutate

useMutation을 이용해 작성한 내용들이 실행될 수 있도록 도와주는 trigger 역할

useMuatiton 정의 후 이벤트가 발생할 때 mutate를 사용한다.

  • 예시
//정의
const { mutate: likedProjectMutate } = useMutation(() =>
    updateMyProject(uid, pid),
  );

//실행
useEffect(() => {
    likedProjectMutate(uid, pid); 
  }, [isLike]);

try, catch, finally ⇒ onSuccess, onError, onSettled

  • try~catch 문
try {
    const res = await axios.post('http://localhost:8080/savePerson', person);

    if(res) {
        console.log('success');
    }
} catch(error) {
    console.log('error');
} finally {
    console.log('finally');
}
  • useMutation 사용
const savePerson = useMutation({
    mutationFn: (person: Iperson) => axios.post('/savePerson', person),
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
})

InvalidateQueries

useQuery에서 사용되는 queryKey의 유효성을 제거해서 refetching하기 위해 사용한다.

데이터 추가 등의 데이터 변화가 일어나는 동작을 실행했을 때

사용자의 입장에서는 화면에 바로 표시되는 것을 바라는데

useQuery에는 staleTime과 cacheTime이 있기 때문에 정해진 시간에 도달하지 않으면

새로운 데이터가 적재 되었더라도 useQuery는 변동 없이 동일한 데이터를 화면에 보여줄 것이다.

이를 해결할 수 있는 것이 바로 invalidateQueries이다.

useMutation을 통해 데이터를 저장할 때 invalidateQueries를 이용해 useQuery가 가지고 있던

queryKey의 유효성을 제거해주면 캐싱되어있는 데이터를 화면에 보여주지 않고 서버에 새롭게 데이터를 요청하게 된다.

  • 예시
const queryClient = useQueryClient();  // 등록된 quieryClient 가져오기

const savePerson = useMutation((person: Iperson) => axios.post('http://localhost:8080/savePerson', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
        queryClient.invalidateQueries('persons'); // 기존에 화면에 뿌려주고 있던(fetching 돼있던) queryKey 유효성 제거
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
}); // useMutate 정의

참고 setQueryData

InvalidateQueries와 다른 데이터 업데이트 방법으로 기존 queryKey에 매핑돼있던 데이터를 새롭게 정의한다.

  • 예시
const queryClient = useQueryClient();  // 등록된 quieryClient 가져오기

const savePerson = useMutation((person: Iperson) => axios.post('http://localhost:8080/savePerson', person), {
    onSuccess: () => { // 요청이 성공한 경우
        console.log('onSuccess');
        queryClient.setQueryData('persons', (data) => {
            const curPersons = data as AxiosResponse<any, any>; // persons의 현재 데이터 확인
            curPersons.data.push(person); // 데이터 push

            return curPersons; // 변경된 데이터로 set
        })
    },
    onError: (error) => { // 요청에 에러가 발생된 경우
        console.log('onError');
    },
    onSettled: () => { // 요청이 성공하든, 에러가 발생되든 실행하고 싶은 경우
        console.log('onSettled');
    }
}); // useMutate 정의

 

 

useQuries

여러개의 useQuery를 병렬적으로 수행하되 유동적으로 변화는 쿼리 작업을 수행할 때 사용한다.

Manulal Parallel Queries

유동적이지 않은 데이터를 조회할 때의 병렬 처리는 useQuery를 순서대로 작성하는 것과 useQueries를 쓰는 것이 차이가 없기 때문에 둘 중 어느 것을 사용해도 상관이 없다. 공식 문서에서도 이렇게 Manual 한 Query를 작성할 때는 useQuery를 원하는 만큼 나열식으로 작성하라고 말하고 있다.

const res1 = useQuery(query1)
const res2 = useQuery(query2)
또는
const result = useQueries([query1, query2])

Dynamic Parallel Queries

function App({ users }) {
  const userQueries = useQueries({
    queries: users.map((user) => {
      return {
        queryKey: ['user', user.id],
        queryFn: () => fetchUserById(user.id),
      }
    }),
  })
}

위와 같은 상황에서 users의 length가 1일지 2일지는 모른다.

이렇게 실행해야 하는 쿼리 수가 렌더링마다 변경되는 경우에는 useQuery를 이용해 미리 나열해 둘 수 없으므로 useQueries를 사용할 수 있다.

또 공식문서에서도 훅 규칙을 위반하니 Manual하게 쿼리를 작성할 수 없다고 한다.

If the number of queries you need to execute is changing from render to render, you cannot use manual querying since that would violate the rules of hooks.

Suspense

useQueries를 사용해야하는 또 다른 상황은 useQuery를 suspense모드로 동작시킬 때이다.

React Query의 suspense 모드를 설정하게 되면 useQuery의 status, error 등을 React.Suspense로 대체해준다.

  • React.Suspense란Suspense라는 React의 신기술을 사용하면 컴포넌트의 랜더링을 어떤 작업이 끝날 때까지 잠시 중단시키고 다른 컴포넌트를 먼저 랜더링할 수 있습니다. 이 작업이 꼭 어떠한 작업이 되어야 한다는 특별한 제약 사항은 없지만 아무래도 REST API나 GraphQL을 호출하여 네트워크를 통해 비동기로(asynchronously) 데이터를 가져오는 작업을 가장 먼저 떠오르게 됩니다.
  • Suspense는 어떤 컴포넌트가 읽어야 하는 데이터가 아직 준비가 되지 않았다고 리액트에게 알려주는 새로운 매커니즘이다.
 

React Suspense 소개 (feat. React v18)

Engineering Blog by Dale Seo

www.daleseo.com

suspense 모드에서 useQueries를 사용해야 하는 이유는 useQuery를 병렬로 처리하여 사용하고 있을 때 만약 하나의 useQuery가 정상적으로 동작되지 않을 경우 그 이후에 실행될 useQuery에 영향 을 미치며 결과적으로 올바른 화면이 보이지 않기 때문이다.

 

번외) 이런 기능도 있다!

prefetchQuery

query의 데이터를 원하는 시점에 미리 로드하는 로직이 필요한 경우에는 queryClient 의 prefetchQuery라는 메소드를 사용하게 되는데, 주로 server side에서 데이터를 미리 받아 오거나, 화면 전환시 컴포넌트 마운트 전에 데이터를 미리 받아오기 위해 사용한다.

 

React-Query 어떻게 써야 할까요? | Life Is Egg 개발일지

요즘 프론트엔드 개발 트렌드는 Redux의 세상에서 벗어나 새로운 상태관리 라이브러리들을 많이 도입하고 있는데요, 그 중 많은 선택을 받고 있고, 개인적으로도 선호하는 라이브러리인 React-Query

lifeisegg-blog.vercel.app

useInfiniteQuery

useInfiniteQuery를 사용하면 무한스크롤을 쉽게 구현할 수 있다고 하네용!

반응형

댓글