React Query

uk·2023년 9월 21일
0

React

목록 보기
16/17

React Query란?

React Query는 데이터 페칭, 캐싱, 서버 데이터 업데이트 및 동기화, 로딩 및 오류 상태를 쉽게 관리할 수 있도록 도와주는 라이브러리이다.
-> 서버 상태(비동기 데이터) 관리 라이브러리

다른 복잡한 데이터 페칭 방식의 단점을 보완하고 간단하고 직관적으로 비동기 API 통신을 할 수 있도록 다양한 훅과 유틸리티를 제공한다.

React Query를 사용하는 이유
Redux, Mobx 등 기존의 전역 상태 관리 라이브러리는 상태가 추가될 때마다 많은 양의 보일러 플레이트 코드가 생기고 클라이언트 상태 외에 서버 상태와 비동기 통신 로직까지 처리해야 하기 때문에 복잡성이 증가하고 유지보수가 힘들어진다.

React Query를 사용하면 이러한 문제를 해결하고 클라이언트 상태와 서버 상태를 분리하여 편리하게 관리할 수 있다.

장점

  1. 캐싱 - 캐싱을 통해 동일한 데이터에 대한 중복 요청을 제거하고 불필요한 API 호출을 최소화하여 성능을 향상시킨다.

  2. 클라이언트 상태와 서버 상태 분리 - Redux와 같은 상태 관리 라이브러리에서 비동기 함수를 처리하기 위한 미들웨어가 제공되지만 로직을 직접 구현해야 하고 클라이언트 상태 관리에 특화되어 있기 때문에 React Query를 통해 서버 상태를 분리하고 효율적으로 관리할 수 있다.

  3. 신선한(fresh) 데이터와 오래된(stale) 데이터를 구분하고 업데이트한다.

  4. 가비지 콜렉터를 통해 사용되지 않는 데이터를 제거하여 메모리를 관리한다.

불필요한 코드 감소, 업무와 협업의 효율성을 위한 규격화된 방식 제공, 사용자 경험 향상을 위한 다양한 내장된 기능 제공


React Query로 가져온 서버 데이터를 상태 관리 라이브러리를 통해 전역 상태로 관리할 수 있지만 반복적인 데이터 페칭이 발생할 수 있기 때문에 성능이 저하될 수 있다.


클라이언트 상태와 서버 상태

클라이언트 상태 - 브라우저, 애플리케이션, 기기 등 클라이언트에 저장 및 관리되는 데이터
-> 사용자 입력 값(input), UI 변경(모달 열고 닫기)

특징

  1. 클라이언트마다 독립적인 상태를 가지기 때문에 클라이언트 간 상태가 공유되지 않음

  2. 상태가 변경되면 비동기 통신을 통해 서버로 데이터를 전달하고 새로운 데이터를 응답받아 업데이트(동기화)
    -> 클라이언트에서 네트워크 요청이 필요하지 않은 데이터는 서버 상태에 비해 접근 및 업데이트 속도가 빠르다.

  3. 클라이언트에서 접근할 수 있고 변조 및 가로채기 공격에 취약

서버 상태 - 서버에 저장 및 관리되며 비동기 API 호출을 통해 불러오는 데이터
-> 서버와 클라이언트가 비동기적으로 공유하는 데이터 또는 클라이언트에서 보여주기 위해 필요한 데이터(사용자 정보, 상품 정보, 좋아요, 게시글 등)

특징

  1. 다양한 클라이언트 환경에서 서버의 데이터를 공유(중앙 집중화를 통해 일관성과 동기화 유지)하고 업데이트할 수 있다.

  2. 클라이언트에서 비동기 통신을 통해 서버의 데이터를 가져오고 업데이트

  3. 보안 및 데이터 무결성 유지

  4. 영속적인 데이터를 저장

Client 데이터는 상태 관리 라이브러리가 관리하고, Server 데이터는 React-Query가 관리
-> Client 데이터와 Server 데이터를 온전하게 분리


React Query 사용하기

useQuery

useQuery - 서버의 데이터를 요청하는 훅(GET)

const { data, error, isLoading } = useQuery({ queryKey: [‘key’], queryFn, options })

useQuery는 기본적으로 React 컴포넌트가 마운트되면 자동으로 실행된다. 또한 useQuery 실행이 완료되면 객체를 반환하며 구조 분해 할당을 통해 필요한 데이터를 추출해서 사용할 수 있다.

React Query를 사용하지 않으면 데이터, 에러 상태, 로딩 상태를 useState를 통해 관리해야 하지만 React Query에서는 기본적으로 제공되기 때문에 간편하게 관리할 수 있다.

queryKey - 쿼리를 식별하는 고유한 키(배열 형태)이며 Query 요청에 대한 응답 데이터를 캐싱한다. queryKey를 통해 다른 컴포넌트에서 참조가 가능하다.
['getData', id] 형태일 경우 id 값이 변경될 때마다 queryFn이 실행된다.

데이터 페칭을 수행하면 queryKey에 결과값이 캐싱되고 queryFn이 다르더라도 queryKey가 동일하다면 서버에는 하나의 요청만 전달되며 동일한 쿼리 결과가 저장된다.

서로 다른 컴포넌트에서 동일한 queryKey를 사용하는 useQuery의 경우 캐시된 데이터를 우선 사용한다.

queryFn - useQuery가 실행되면 데이터를 불러오는 비동기 요청(fetch, axios)을 수행하는 함수이며 Promise를 반환한다.

  // useQuery의 반환 값을 구조 분해 할당을 통해 받아오기
  const { data, error, isLoading } = useQuery({
    queryKey: ['getData'],
    queryFn: () =>
      fetch('http://example.com/user').then((res) => res.json()),
  });

  // isLoading과 error를 통해 예외 처리
  if (isLoading) return <div>로딩중...</div>;
  if (error) return <div>{error.message}</div>;
}

data - 쿼리가 성공적으로 실행되면 반환된 데이터가 저장됨
error - 쿼리 실행 중 발생한 오류가 저장됨
isLoading - 퀴리가 현재 로딩 중인지 여부를 boolean 값으로 나타냄
isError - 쿼리 결과가 오류인지 여부를 boolrean 값으로 나타냄

useQuery의 콜백

useQuery의 onSuccess, onError, onSettled와 같은 콜백 함수들은 동작이 일관되지 않기 때문에 React Query v5에서 deprecated되었다.

동일한 쿼리에 대해 여러 번 호출될 수 있고 side effect 및 상태 업데이트가 중복될 수 있다.
-> 동일한 쿼리에 대해 2개의 중복된 오류 알림이 표시

import { QueryCache, QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  queryCache: new QueryCache({
    onSuccess: () => { ... },
    onError: () => { ... },
    onSettled: () => { ... },
  }),
});

QueryClient의 QueryCache를 통해 전역에서 콜백을 사용해 제어할 수 있다.


return 키워드

const fetchData = () => {
  return fetch('http://example.com/user').then((res) => res.json());
};

// 단일 표현식인 경우 중괄호, return 생략 가능
const fetchData = () => 
  fetch('http://example.com/user').then((res) => res.json());


const { data, error, isLoading } = useQuery({
  queryKey: ['getData'],
  queryFn: fetchData,
});

useQuery의 queryFn은 Promise를 반환해야 하기 때문에 데이터 페칭 함수에 중괄호와 return 키워드를 넣어줘야 한다.

단일 표현식인 경우 중괄호와 return 키워드 모두 생략이 가능하지만 중괄호가 존재할 때 return 키워드를 추가하지 않으면 data는 undefined를 반환한다.


enabled

const { data, error, isLoading } = useQuery({
  queryKey: ['getData'],
  queryFn: fetchData,
  enabled: !!userId,
});

useQuery는 기본적으로 React 컴포넌트가 마운트되면 자동으로 실행되기 때문에 enabled(boolean) 속성을 사용하면 useQuery를 조건부(자동으로 실행되지 않도록)로 실행시킬 수 있다.
-> userId 값이 있을 경우에만 실행

Refetch

useQuery는 컴포넌트가 처음 마운트될 때 자동으로 데이터를 페칭하고 이후 서버의 데이터가 업데이트 되어도 자동으로 가져오지 않는다.

refetch 옵션은 캐싱된 데이터가 만료(stale)되고 특정 조건을 만족할 경우 서버에서 최신 데이터를 다시 가져온다.

const { data, error, isLoading } = useQuery({
  queryKey: ['getData'],
  queryFn: fetchData,
  refetchOnMount,  // default: true
  refetchOnReconnect,  // default: true
  refetchOnWindowFocus,  // default: true
  refetchInterval: 3000,  // default: false
  refetchIntervalInBackground: true,  // default: false
});

refetchOnMount (boolean | 'always') - 데이터 만료 && React Query를 사용하는 컴포넌트 마운트 시 refetch
refetchOnReconnect (boolean | 'always') - 데이터 만료 && 네트워크 재연결 시 refetch
refetchOnWindowFocus (boolean | 'always') - 데이터 만료 && 브라우저 포커스 시 refetch
refetchInterval (number | false) - 일정한 간격으로 refetch (Polling)
refetchIntervalInBackground (boolean) - 브라우저가 백그라운드(focus 비활성)에 있는 동안 일정한 간격으로 refetch

-> 'always' 값을 줄 경우 데이터 만료(stale)와 관계 없이 조건이 만족할 때마다 데이터를 refetch한다.

*Poliing - 일정한 주기를 가지고 서버와 통신하는 방식, 주기적으로 데이터를 가져오는 방식

Query Caching

React Query는 쿼리 결과를 자동으로 캐싱하고 동일한 요청에 대해 캐시된 데이터를 사용하여 성능을 최적화한다. (queryKey를 통해 캐싱)
-> 데이터 로드 시간 감소, 중복 요청 방지, 불필요한 네트워크 호출 최소화

fresh - 신선한, 최신의 데이터 -> fresh한 데이터는 refetching을 수행하지 않고 캐싱된 데이터를 재사용

stale - 신선하지 않은, 오래된 데이터 -> stale한 데이터는 컴포넌트가 다시 마운트 되거나 네트워크 재연결, 브라우저 포커스 등의 트리거가 발생했을 때 refetching을 수행 (데이터가 stale한 상태여도 바로 refetching을 수행하지 않음)

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

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 1000 * 30,  // default: 0
      gcTime: 1000 * 60 * 3, // default: 300000
    },
  },
});

return (
  <QueryClientProvider client={queryClient}>
    <Component />
  </QueryClientProvider>
)

// 각 쿼리마다 설정
const { data, error, isLoading } = useQuery({
  queryKey: ['getData'],
  queryFn: fetchData,
  // 30초간 페이지를 이동해도 refetching을 수행하지 않음
  staleTime: 1000 * 30,  // default: 0
  // 쿼리 결과가 Inactive되어도 5분간 캐시에서 삭제되지 않음
  gcTime: 1000 * 60 * 3, // default: 300000
});

staleTime (number | Infinity) - 데이터가 fresh -> stale 상태로 변경되는데 걸리는 시간을 설정 (fresh 상태로 유지되는 시간)

  1. React Query에서는 기본적으로 서버로부터 가져온 데이터를 stale한 상태로 여기며 staleTime이 지나기 전까지는(fresh 상태) 컴포넌트가 다시 마운트 되어도 refetching을 수행하지 않는다.

  2. staleTime: 5000으로 설정한다면 데이터 페칭 성공 후 쿼리 결과가 5초 동안 fresh 상태가 되었다가 5초 이후에는 stale 상태가 되고 특정 조건을 만족할 경우 데이터를 refetching한다.

  3. stale이 필요한 이유 - stale 상태의 캐싱된 데이터를 보여주면서 refetching이 완료되면 새로운 데이터로 교체한다.
    -> cache만 사용할 경우 refetching을 통해 새로운 데이터로 교체하기 전에 로딩 상태를 나타내야한다.

gcTime (number | Infinity) - 사용되지 않는 쿼리 결과(Inactive)가 캐시에 유지되는 시간을 설정, 데이터를 gcTime 만큼 메모리에 저장하고 이후 사용되지 않을 경우 데이터를 삭제한다.

  1. 해당 쿼리를 사용하는 컴포넌트가 언마운트되는 시점에 Inactive 상태가 되고 캐시는 gcTime만큼 유지된다.
    -> staletime과 관계없이 Inactive 시점을 기준으로 캐시를 제거한다.

  2. gcTime이 지나기 전에 컴포넌트가 다시 마운트 되면 데이터를 refetching하는 동안 캐싱된 데이터를 보여준다.

  3. gcTime은 쿼리가 동작하는 동안에는 어떠한 영향도 주지 않으며 gcTime이 지나면 해당 데이터는 가비지 콜렉터에 의해 메모리 상에서 제거된다.

-> React Query v5에서 cacheTime -> gcTime 변경

gcTime을 0으로 설정한다면 페이지 이동 시 컴포넌트가 언마운트 되기 때문에 캐시 데이터가 즉시 삭제되고 컴포넌트가 다시 마운트되면 데이터 페칭이 수행된다.

또한 staleTime을 gcTime보다 길게 설정하면 staleTime보다 짧은 시간 안에 데이터를 refetching 해야하는 상황이 발생하기 때문에 비효율적이다.

-> gcTime의 기본값(5분)은 변경하지 않고 자주 변경되지 않는 데이터의 경우 staleTime을 조정하여 불필요한 데이터 페칭 줄이는 것이 좋다. 자주 변경되는 데이터는 기본값(0초) 사용

React-Query에서 캐시 상태
inactive -> fetching -> fresh -> stale

useQueries

useQueries - 여러 쿼리를 동시에 실행하고 처리하는 훅(Promise.all())

// 두 쿼리에 대한 반환 값을 배열로 묶어 반환
  const queryResults = useQueries({
    queries: [
      {
        queryKey: ['getData', 1],
        queryFn: () =>
          fetch('http://example.com/user/1').then((res) => res.json()),
      },
      {
        queryKey: ['getData', 2],
        queryFn: () =>
          fetch('http://example.com/user/2').then((res) => res.json()),
      },
    ],
  });

useMutation

useMutation - 서버의 데이터를 변경하는 훅 (POST, PUT, DELETE)

const mutation = useMutation({ mutationFn, options });

mutationFn - 서버의 데이터를 생성, 업데이트, 삭제 요청(side effect)을 수행하는 함수로 Promise를 반환한다.

useMutation은 데이터를 캐싱하지 않기 때문에 queryKey가 필요하지 않다.

 const mutation = useMutation({
    mutationFn: async (data) => {
      await fetch('http://example.com/user', {
        method: 'post',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });
    },
    onMutate: () => { console.log('요청 시작') },
   	onSuccess: () => { console.log('요청 성공') },
  	onError: () => { console.log('에러 발생') },
  	onSettled: () => { console.log('성공, 실패와 관계 없이 실행') }
  });

// mutationFn의 파라미터(data)로 값 전달
mutation.mutate({ ... });

mutate - mutationFn에 정의된 동작을 실행하는 함수로 mutate의 파라미터는 mutationFn의 파라미터로 전달된다.


쿼리 무효화(Query Invalidation)

useMutation을 통해 POST, PUT, DELETE 요청을 수행하고나면 이전의 데이터는 유효하지 않기 때문에 기존에 캐싱된 쿼리 결과를 무효화하고 업데이트된 서버의 데이터를 가져와서 다시 캐싱하는 과정이 필요하다.

invalidateQueries 메서드를 사용하면 queryKey에 캐싱 되어있는 stale한 데이터를 보여주지 않고 서버에서 업데이트된 데이터를 받아와 클라이언트 데이터와 서버 데이터를 동기화 시킬 수 있다.
-> useMutation을 통해 서버 상태 업데이트 후 queryKey에 캐싱된 쿼리 결과를 무효화하고 서버로부터 업데이트된 데이터를 refetching

import { useQueryClient } from '@tanstack/react-query'

// useQueryClient 훅을 사용하여 queryClient 객체 생성
const queryClient = useQueryClient()

// 모든 쿼리 무효화
queryClient.invalidateQueries()

// 특정 키를 가지는 쿼리 무효화
queryClient.invalidateQueries({ queryKey: ['user'] })

queryClient 객체의 invalidateQueries 메서드는 모든 쿼리 또는 고유한 키를 사용하는 특정 쿼리의 결과를 오래된(stale) 상태로 표시하고 사용자가 페이지를 새로고침 할 필요 없이 새로운 데이터를 불러와 갱신한다.

// mutate 함수를 실행할 때 옵션으로 onSuccess 함수를 전달하면서 쿼리를 무효화
mutation.mutate(item.id, {
  onSuccess: () =>
    queryClient.invalidateQueries({ queryKey: ['getData'] }),
});

// useMutation의 옵션으로 요청 성공 시 쿼리 무효화
const mutation = useMutation({
  //...
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['getData'] });
  },
});

낙관적 업데이트(Optimistic Update)

낙관적 업데이트 - 서버로 요청을 보내기 전에 클라이언트 데이터를 미리 업데이트하는 것

서버로 요청을 보내고 응답을 받기 전에 UI를 업데이트 함으로써 지연시간 없이 사용자에게 빠른 피드백 제공할 수 있다.
-> 메세지, 좋아요, 게시글 비공개 등 기존 데이터를 업데이트하는 기능에 사용됨

  useMutation({
    mutationFn: fetchData,
	// 동기(순차)적으로 동작해야 하기 때문에 async/await 사용
    onMutate: async (newData) => {
      // 낙관적 업데이트를 위해 쿼리 요청 취소, refetching 방지
      await queryClient.cancelQueries({ queryKey: ['getData'] });
	  // 이전에 캐싱된 데이터 불러오기
      const previousData = queryClient.getQueryData(['getData']);

      // setQueryData 함수를 통해 캐싱된 데이터를 낙관적 업데이트(불변성 유지)
      queryClient.setQueryData(['getData'], (oldData: Item) => [
        { ...oldData, newData },
      ]);
      // 에러 발생 시 이전으로 되돌리기 위해 이전 데이터를 반환
      return {
        previousData,
      };
    },

    // error 객체, 사용자 입력 데이터, 쿼리 컨텍스트
    onError: (error, newData, context) => {
      queryClient.setQueryData(['getData'], context?.previousData);
    },

    // 쿼리의 성공, 실패 여부와 관계없이 쿼리 무효화를 통해 최신 데이터 불러오기
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['getData'] });
    },
  });

useMutation Hook의 onMutate, onError, onSettled 함수를 통해 낙관적 업데이트를 수행하며 onMutate는 서버로 요청을 보내기 전에 실행된다.

setQueryData 함수를 통해 캐싱된 데이터를 낙관적 업데이트할 때 불변성을 유지시킴으로써 데이터 변경을 감지할 수 있도록 한다.

mutation 요청 중 에러가 발생하면 onError 함수에서 previousData를 통해 이전 상태로 되돌릴 수 있다.

profile
주니어 프론트엔드 개발자 uk입니다.

0개의 댓글