React-query 를 사용해 상태관리를 해보자


프로젝트를 새로 시작함에 있어 상태관리 라이브러리를 찾아 보던 중, Redux나 MobX 같은 라이브러리 들에 비해 러닝커브가 상당히 낮고 보일러플레이트가 적어 편하게 사용할 수 있는 React Query에 대해 알게되었습니다. 이미 무스마의 많은 개발자 분들이 Reacy query를 개발에 잘 활용하고 계시지만, React Query를 학습하며 정리하는 차원에서 기술블로그에 작성해보려 합니다.



목차

  1. 리액트 쿼리를 왜 사용하는가?
  2. 리액트 쿼리를 사용하지 않던 기존의 데이터 패칭 코드
  3. 2번과 같은 기능을하는 리액트 쿼리를 사용한 코드
  4. Query & Mutation
  5. 데이터 캐싱
  6. 마치며




1. 리액트 쿼리를 왜 사용하는가?

React Query는 애플리케이션의 데이터를 관리하고 동기화하는데 사용 되는 라이브러리로 최근 많은 개발자들에게 인기를 얻고 있는 상태관리 라이브러리 입니다. 리액트 쿼리를 사용하는 이유는 크게 다음과 같습니다.

1) 간편한 데이터 관리

데이터 가져오기, 캐싱, 동기화 및 업데이트 처리를 간편하게 할 수있게 해줍니다.

2) 실시간 업데이트 및 동기화

실시간 데이터 업데이트와 자동 동기화를 지원하여 서버와 클라이언트 데이터의 일관성을 유지합니다.

3) 데이터 캐싱

데이터를 캐싱하여 불필요한 API 요청을 줄이고 애플리케이션의 성능을 향상 시킵니다.

4) 서버 상태 관리

서버 상태 관리 (예를들면 로딩중, 에러, 성공 등의 상태)를 간편하게 처리할 수 있습니다.

5) 간편한 설정

React Query는 간단한 설정으로 사용할 수 있습니다.

리액트 쿼리를 사용하기 전에 먼저 @tanstack/react-query와 devtools 를 설치해줍니다.

(devtools 는 선택사항이며 devtools을 사용하면 패칭한 데이터를 쉽게 관리할 수 있습니다.)

npm install @tanstack/react-query @tanstack/react-query-devtools
pnpm add @tanstack/react-query @tanstack/react-query-devtools

리액트 쿼리를 설치한 후에 아래와 같이 세팅을 하여 사용할 준비를 해줍니다.

최상위 컴포넌트 위에 QueryClientProvider로 감싸줘야 합니다.

(devtools는 최상위 컴포넌트에 최대한 가까운 위치로 배치해주시면 됩니다.)

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

const queryClient = new QueryClient()

// 아래와 같이 QueryClient에 defaultOptions를 설정해서 사용할 수도 있습니다.
// const queryClient = new QueryClient({
//   defaultOptions: {
//     queries: {
//       suspense: true,
//     },
//   },
// })

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Todos />
			<ReactQueryDevtools /> // 선택사항
    </QueryClientProvider>
  )
}

devtools를 설치하게 되면 화면 하단에 꽃모양의 아이콘이 생기게 됩니다.

Untitled

클릭해보면 현재 불러오고 있는 데이터들이 무엇이 있는지 한눈에 보며 관리할 수 있습니다. Untitled




2. 리액트 쿼리를 사용하지 않던 기존의 데이터 패칭 코드

React-query를 사용하지 않던, 기존의 data fetching 로직을 살펴 보겠습니다.

1) Fetching 코드 작성

const getNikaData = async () => {
  const data = await fetch("https://musma.net/user/nika")
		.then((response) => response.json());
  return data;
};

2) 데이터를 담아 둘 상태 생성

const [nika, setNika] = useState();

3) useEffect를 이용해 컴포넌트 마운트시 데이터 Fetching 후 상태 저장

useEffect(() => {
    getNikaData()
      .then((res) => setState(res))
      .catch((err) => {
				console.log(err.response.data)
			});
  }, []);

하나의 data를 패칭함에 있어 작성되는 코드라인 수가 많을 뿐더러, useEffect를 많이 사용하기 때문에 발생할 수 있는 side effect로 인해 코드의 흐름 파악이 어려울 수 있습니다. 특히나 한 페이지내에서 데이터 패칭을 하는 로직이 여러개가 된다면 관리하기 어렵고 코드 가독성을 더 떨어질 것 입니다.




3. 2번과 같은 기능을하는 리액트 쿼리를 사용한 코드

위의 코드를 React-query를 사용하면 아래와 같이 바꿀 수 있습니다.

1) Fetching 코드 작성

const getNikaData = async () => {
  const data = await fetch("https://musma.net/user/nika")
		.then((response) => response.json());
  return data;
};

2) 데이터 Fetcing 및 상태 저장

const { data } = useQuery(["nika"], getNikaData);

2번의 코드에 비해 코드라인 수가 훨씬 감소 하였고, 데이터 패칭 로직을 따로 분리한다면 데이터를 사용하는 컴포넌트에 작성되는 코드는 useQuery()를 사용하는 한 줄 밖에 쓰이지 않습니다. 또한 useEffect를 사용하지 않음으로써 코드의 흐름을 파악하기 쉬워졌습니다.




4. Query & Mutation

React Query에서 자주 사용되는 주요 기능들에 대해 알아보겠습니다. React Query는 API의 요청을 Query, Mutation의 두가지로 처리합니다.


1) Query

Query 함수를 사용해서 데이터를 가져오고 캐싱할 수 있습니다. Query 함수는 데이터 패칭용으로 사용되며, 보통은 GET으로 받아오는 대부분의 API에서 useQuery() 함수를 사용합니다.

가장 기본적인 형태의 useQuery 요청 형태는 다음과 같습니다.

const { data } = useQuery(
	queryKey,
	queryFunction,
	options,
)

첫번째 인자는 Query Key로 응답 데이터의 Unique key 입니다. 응답 데이터를 캐싱할 때 사용됩니다.

두번째 인자는 Query Function으로 Promise를 반환하는 함수입니다. 이 쿼리 요청을 수행하기 위한 fetch, axios등의 함수를 의미합니다.

세번째 인자는 useQuery에 사용되는 옵션을 지정하는 객체입니다.

useQuery를 실행하면 다양한 리턴값을 받을 수 있는데, 그 중 자주 사용하는 것들에 대해 살펴보겠습니다.

data : 마지막으로 성공한 데이터 (Response)

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

isFetching, isLoading, isSuccess 등 : 현재 Query의 상태

refetch : 해당 Query를 refetch 하는 함수

이외에도 많은 리턴값들이 있으니 공식 문서를 참고하시길 바랍니다.

아래는의 코드는 useQuery를 사용하여 user의 프로필 정보를 가져오는 예시입니다.

import { useQuery } from 'react-query';

// 데이터 패칭 함수 
const getUserData(userId) = () => {
  return fetch(`/api/user/${userId}`).then((response) => response.json());
}

const UserProfile = ({ userId }) => {
	// useQuery 를 사용하여 데이터를 가져옵니다. ['user', userId] 는 쿼리 키를 의미합니다.
  const { data, isError, error, isLoading } = useQuery(['user', userId], () => fetchUserData(userId));

  if (isLoading) { // isLoading을 사용하여 데이터가 로딩중일 때 화면을 랜더링합니다.
    return <div>Loading...</div>;
  }

  if (isError) { // isError를 사용하여 error가 발생할 때 화면을 랜더링합니다.
    return <div>Error: {error.message}</div>;
  }

  return (
    <div>
      <h1>User Profile</h1>
      <p>Name: {data.name}</p>
      <p>Email: {data.email}</p>
    </div>
  );
}


2) Mutation

Mutaion 함수를 사용하여 데이터를 업데이트하고 캐시를 업데이트 할 수 있습니다. Mutation 함수는 데이터 생성, 수정, 삭제 용으로 사용되며 POST, PUT, DELETE 요청시에 사용합니다.

가장 기본적인 형태의 useMutation 요청 형태는 다음과 같습니다.

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

첫번째 인자는 Mutation Function으로 Promise를 반환하는 함수입니다. useQuery 에서와 마찬가지로 fetch, axios등의 함수를 의미합니다.

두번째 인자는 useMutation에 사용되는 옵션을 지정하는 객체입니다.

(Query Key를 설정해줄수도 있으며, Query Key를 지정하게되면 devtool 에서 볼 수 있습니다.)


아래 코드는 useMutation을 사용하여 사용자의 정보를 업데이트하는 예시입니다.

import { useMutation, useQueryClient } from 'react-query';

// 데이터 업데이트 함수
function updateUser(userId, updatedData) {
  return fetch(`/api/user/${userId}`, {
    method: 'PUT',
    body: JSON.stringify(updatedData),
  }).then((response) => response.json());
}

function UserProfileEditor({ userId }) {
  const queryClient = useQueryClient();
  
  const { mutate } = useMutation((updatedData) => updateUser(userId, updatedData), {
    onSuccess: () => {
      // 데이터 업데이트 후 캐시를 재로드
      queryClient.invalidateQueries(['user', userId]);
    },
  });

  const handleSubmit = (updatedData) => {
    mutate(updatedData); // mutate는 자동으로 실행되지 않기 때문에 submit 시에 mutate 실행
  };

  return (
    <div>
      <h2>Edit User Profile</h2>
      <UserForm onSubmit={handleSubmit} />
    </div>
  );
}




5. 데이터 캐싱

React Query의 데이터 캐싱에 대해 알아보겠습니다. staletime, cachetime 옵션을 사용하면 React Query에서 자체적으로 제공하는 데이터 캐싱 기능을 이용할 수 있습니다. 캐싱을 활용하여 불필요한 API 호출을 줄임으로써 전체적인 애플리케이션 성능을 향상 시킬 수 있습니다.


2) staletime (갱신 지연 시간)

staletime은 캐시된 데이터의 유효 기간을 나타내는 옵션입니다. 호출한 데이터는 리액트 쿼리 자체적으로 저장하는데 staletime은 기본적으로 0으로 설정되어 있어, 데이터가 한 번 캐시되면 즉시 만료되고 다시 요청됩니다.

이 값을 조정해서 데이터를 일정 시간 동안 캐시로 사용하고, 그 이후에만 다시 요청하도록 할 수 있습니다. 쉽게 말하면 데이터의 유통기한을 정해준다고 할 수 있습니다.

const { data } = useQuery(['data', getServerData,{
  staletime: 10 * 60 * 1000;
})

자주 변경되지 않는 데이터의 경우 캐시된 데이터를 사용함으로써 불필요한 네트워크 요청을 줄이고, 애플리케이션의 성능을 향상시킬 수 있습니다.


2) cachetime (캐시 유지 시간)

cachetime은 캐시된 데이터가 얼마나 오랫동안 메모리에 유지될지를 나타내는 옵션입니다.

이 값을 설정하면 캐시된 데이터가 일정 시간 도안 메모리에 유지된 후 자동으로 삭제됩니다.

const { data } = useQuery(['data', getServerData], {
  cachetime: 30 * 60 * 1000, // 30분
})

데이터가 일정 기간 동안 메모리에 남아 있게 함으로써 같은 데이터를 다시 요청할 때 캐시에서 가져오기 때문에 성능을 향상 시킬 수 있습니다.




6. 마치며

이상으로 React Query의 주요 기능들을 정리해보았습니다. 저는 여러 상태관리 라이브러리를 써보진 않았지만 React Query는 Redux와 같은 다른 상태관리 라이브러리 들에 비해 직관적이고 간단하게 사용이 가능하며 기능 또한 강력하다 라는 것을 많이 느꼈습니다.

다음에 기회가 된다면 Reacy Query와 함께 사용하고 있는 클라이언트 전역 상태관리 라이브러리인 zustand에 대해 소개해보겠습니다. 읽어주셔서 감사합니다!