React Query 라이브러리의 사용법 및 특징: 서버 상태 관리

2024. 1. 18. 14:17Front-end/React.js

반응형

React Query

React 프로젝트에서 부모-자식 관계 이외의 여러 컴포넌트간에 상태가 공유되어야 할 때가 있습니다. 이 문제를 해결하기 위해 React에 기본 내장된 전역 상태 공유 기능인 Context API부터 외부의 상태 관리 라이브러리인 Redux, Recoil, Zustand까지 종류가 다양합니다.

그러나 이는 클라이언트의 상태 관리에만 초점을 맞춘 경향이 있으며 서버에 데이터를 fetch하고 데이터를 캐싱하는 등 서버 상태까지 관리하기에는 적합하지 않을 수 있습니다. Redux의 경우에는 미들웨어인 Redux-thunk 등을 활용하여 서버 상태를 관리하기도 하지만 상대적으로 복잡하다는 단점이 있습니다.

 

React Query는 서버 상태, 즉 서버에서 불러운 데이터를 관리하기에 적합한 상태 관리 라이브러리입니다.

서버에서 데이터를 fetch해야하는 상황이라면 보통 useEffect를 떠올리기 쉽지만, useEffect는 컴포넌트가 마운트되고 해제되는 생명주기에 관여하며 절차적 프로그래밍과 밀접한 관련이 있습니다. 코드의 가독성이나 유지보수, 부수 효과(side effect) 문제도 있고 useEffect에서는 상태의 흐름을 예측하기가 어렵기 때문에 요즘의 선언형 프로그래밍 트렌드와는 동떨어져있고 useEffect 사용을 지양하는 편이 좋습니다.

 

또한 React Query는 데이터를 최신 상태로 자동으로 업데이트 해주거나, 화면이 전환되었을때 업데이트 하거나, 기존에 캐싱된 데이터를 제공하는 등 서버 상태를 관리하기에 적합하며 서버에 요청한 데이터의 로딩 상태 및 에러 상태를 쉽게 알 수 있는 장점이 있습니다. 


React Query 사용법

1. React Query를 설치합니다.

$ npm i @tanstack/react-query

 

2. React 프로젝트를 생성하고 최상위의 index 파일에서 <App /> 컴포넌트를 <QueryClientProvider /> 로 감싸줍니다.

import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import App from '_app';

const queryClient = new QueryClient();

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <StrictMode>
    <QueryClientProvider client={queryClient}>
        <App />
    </QueryClientProvider>
  </StrictMode>,
);

 

3.  데이터를 fetch하는 컴포넌트에서 useQuery()를 사용합니다.

 

useQuery

  • 첫 번째 파라미터로는 Query Key가 포함된 배열이 필요합니다. Query Key는 캐시 관리를 할 때 사용됩니다.
    useQuery에서 사용할 Query Function에 파라미터가 필요하다면 이 배열에 미리 두번째 인자로 전달해야합니다.
  • 두 번째 파라미터로는 실제 호출하고자 하는 비동기 함수인 Query Function이 필요합니다. 이 함수는 Promise를 반환해야 합니다.
  • 세 번째 파라미터로는 여러 옵션을 선택할 수 있습니다. 자주 쓰이는 옵션은 밑에 정리했습니다. 
  • useQuery가 반환하는 값은 데이터, 로딩 여부, 실패 여부 등으로 다양합니다.

 

[옵션]
refetch : 해당 query를 refetch 하는 함수
retry: query 동작 실패시 자동으로 몇번 retry 할껀지
select: response 값에서 필요한 값만 추출
enabled: 동기적으로 query를 실행할지 여부. 예를 들어, 이전의 fetch가 완료되면 다음의 fetch를 실행되게 할 수 있음
refetchInterval: error boundary로 에러를 전파할 지 결정하는 옵션

 

[반환값]

data : 마지막으로 resolved된 데이터

error : 에러가 발생했을 때 반환되는 객체

isFetching: fetch가 resolved될때마다 true 반환 -> cache가 있어도 true

isLoading: 최초 fetch가 resolved되지 않고 in-flight 상태일 때 true 반환 -> cache가 있으면 no

isSuccess: resolved되면 true

 

function App() {
  const fetchData = async () => await axios.get(`/api/data/list`)

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

  return (
    <></>
  );
}
export default App;

 

4.  여러 쿼리를 한 번에 사용해야 한다면 useQueries를 사용합니다. 

function App() {
 const fetchData = async () => await axios.get(`/api/data/list`)
 const fetchPost = async () => await axios.get(`/api/post/list`)

 const { data, isLoading, error } = useQueries({
  queries: [
    { queryKey: ['key-data'], queryFn: fetchData},
    { queryKey: ['key-post'], queryFn: fetchPost},
      ...
    ]
  })

  return (
    <>
    </>
  );
}
export default App;

 

5. POST, PUT, DELETE처럼 서버의 상태(데이터)를 수정할때는 useMutation()을 사용합니다.

 

useMutation

  • useQuery와 다르게 Query Key는 필요하지 않습니다. 캐싱 기능이 필요하지 않기 때문입니다.
  • useMutation을 정의하고 mutate로 실행합니다.
  • 서버 상태의 변경이 일어나기 때문에 onSuccess 등의 옵션을 사용해서 기존의 queryKey에 해당하는 데이터를 갱신시키는게 일반적입니다.
  • 사용자에게 업데이트된 UI를 먼저 보여주고 그 다음에 서버의 데이터를 업데이트하는 '낙관적 업데이트'가 가능합니다. 옵션의 onMutate를 사용합니다.
function App() {

  const deleteData = async (id: number) => axios.delete(`api/delete/${id}`);

  const { mutate, isLoading, error } = useMutation({
    mutationFn: deleteData,
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ['key-data']});
    }
  });

  return (
    <>
      <button onClick={mutate(123)}/>
    </>
  );
}
export default App;

 


React Query의 데이터 갱신: staleTime과 cacheTime 

React Query의 핵심은 데이터 갱신을 언제 어떻게 할지 결정할 수 있다는 점입니다. 사용자에게 로딩중인 화면을 보게 하는것보다는 오래된 데이터라도 보여주는것이 나을 때도 있습니다. 이는 사용자 경험과 밀접하기 때문에 중요합니다.

React Query에서는 데이터를 막 fetch한 싱싱한 상태를 fresh라고 정의합니다. 일정 시간(staleTime)이 지나면 데이터는 fresh에서 stale로 전환된다고 정의합니다. 기본 값은 0으로 설정되어 있으며 이는 데이터가 항상 stale이기 때문에 refetch시 항상 새로운 데이터를 가져오게 됩니다.

 

refetch는 다음과 같은 상황에 발생합니다.

1. refetchOnWindowFocus: 브라우저 창에 포커스가 들어온 경우, 기본값 true

2. refetchOnMount: 새로운 컴포넌트 마운트가 발생한 경우, 기본값 true

3. refetchOnReconnect: 네트워크 재연결이 발생한 경우, 기본값 true

 

즉, 데이터가 fresh상태라면 이러한 상황이라도 새로운 데이터를 가져오지는 않습니다.

 

컴포넌트가 unmount되어서 데이터가 inactive한 상태라면 데이터를 사용하지도 않는데 메모리에 계속 상주하는 상태가 됩니다. 따라서 inactive로 전환된 뒤 일정 시간 이후에 메모리에서 해제해주는 Garbage Collection이 필요합니다. 

이 시간을 React Query의 v5 이전 버전에서는 cacheTime, 이후 버전에서는 gcTime이라고 합니다.

만약 컴포넌트가 다시 마운트되었다면 메모리에 존재하던 inactive 데이터가 다시 보이고 이 상태에서 새로운 데이터를 가져오기 위해 fetch를 시도합니다.

이러한 staleTime과 cacheTime은 useQuery와 useMutation의 옵션으로 수정할 수 있습니다.

 

[참고자료]

 

반응형