본문으로 바로가기

  
 
저는 개인적으로 운영 중인 프로젝트에서는 API 통신과 비동기 데이터 관리를 위해 React Query를 적극적으로 활용하고 있습니다. 프로젝트를 처음 진행할 당시에는 Redux의 redux-saga를 주로 사용하여 서버와의 API 통신과 비동기 데이터를 관리하였습니다. 그러다가 불편함을 느끼고 React Query를 도입하여 Redux에서 React Query로 마이그레이션하였습니다. 이번 글에서는 왜 React Query도입을 결정하게 되었는지 정리하려고 합니다.
 
 

Redux는 Global State Management!

Redux는 모두가 알다시피 Redux의 기본 원칙이 존재합니다. 단일 스토어여야 하며, 상태는 불변성을 유지해야 하기 때문에 읽기 전용이어야 하며 순수함수로 상태를 변경해야 합니다. 이 기본 원칙을 지키기 위해서는 너무나 장황한 Boilerplate 코드가 요구됩니다. rtk(redux-toolkit)의 등장으로 그나마 Boilerplate 코드가 많이 줄긴 했지만, Redux로 비동기 데이터를 관리하는 일에는 여전히 많은 Boilerplate 코드가 요구됩니다. 아래의 코드는 Redux Saga로 데이터를 가져오는 예제 코드입니다.

// actions.js
export const FETCH_DATA = 'FETCH_DATA';
export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS';
export const FETCH_DATA_FAILURE = 'FETCH_DATA_FAILURE';

export const fetchData = () => ({ type: FETCH_DATA });


// reducers.js
const initialState = {
  data: [],
  loading: false,
  error: null,
};

export const dataReducer = (state = initialState, action) => {
  switch (action.type) {
    case FETCH_DATA:
      return { ...state, loading: true, error: null };
    case FETCH_DATA_SUCCESS:
      return { ...state, loading: false, data: action.payload };
    case FETCH_DATA_FAILURE:
      return { ...state, loading: false, error: action.payload };
    default:
      return state;
  }
};


// sagas.js
import { takeLatest, call, put } from 'redux-saga/effects';
import { FETCH_DATA, FETCH_DATA_SUCCESS, FETCH_DATA_FAILURE } from './actions';
import api from './api'; // Assume there's an API utility

function* fetchDataSaga() {
  try {
    const data = yield call(api.getData);
    yield put({ type: FETCH_DATA_SUCCESS, payload: data });
  } catch (error) {
    yield put({ type: FETCH_DATA_FAILURE, payload: error.message });
  }
}

export function* watchFetchData() {
  yield takeLatest(FETCH_DATA, fetchDataSaga);
}


// App.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchData } from './actions';

const App = () => {
  const dispatch = useDispatch();
  const { data, loading, error } = useSelector((state) => state.data);

  useEffect(() => {
    dispatch(fetchData());
  }, [dispatch]);

  if (loading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error}</p>;
  }

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default App;

 
또한, Redux는 API 통신 및 비동기 상태 관리를 위한 라이브러리가 아닙니다. 전역 상태를 관리하는 Redux를 사용하여 비동기 데이터를 처리할 경우 Redux Thunk, Redux Saga와 같은 미들웨어를 필요로하여 사용하지만, Redux를 사용하여 비동기 데이터를 관리하기 위해서는 관련된 코드를 하나부터 열까지 개발자가 결정하고 구현해야 합니다. API 응답값을 전부 State에 보관하고 필요한 값만 사용할 수도 있고 화면에 뿌려줄 수도 있습니다. 이는 Redux가 비동기 데이터를 관리하기 위한 전문 라이브러리가 아니라, 범용적으로 사용할 수 있는 전역 상태 관리 라이브러리여서 생겨나는 현상입니다. 이러한 방식과 방법에 정답은 없지만, 팀의 구성원이 많아지고 협업 관계가 복잡하게 구성될수록 자연스러운 방향으로 통일된다면 더 효율적인 업무가 가능할 것입니다.
 
 

React Query란?

위와 같은 Redux를 사용한 API 요청과 비동기 데이터 관리의 불편함을 해소하기 위해 저는 전향적으로 React Query를 도입하여 사용하고 있습니다. React Query는 리액트에서 데이터 관리와 상태 관리를 쉽게 처리할 수 있도록 도와주는 라이브러리입니다. 서버에서 데이터를 가져오고 캐싱하며, 컴포넌트 간에 상태를 쿼리하고 공유하는 데 사용됩니다. React Query는 주로 비동기 데이터를 다루는데 중점을 두며, API 호출, 상태 업데이트, 캐싱, 지속적으로 동기화, 재시도 및 인터벌 등 다양한 기능을 제공합니다.
 
또한, React Query가 얼마나 간단하게 API를 요청하고 상태관리가 쉬운 지 코드를 보며 확인하겠습니다. 아래는 React Query로 데이터를 가져오는 예제입니다.

// App.js
import React from 'react';
import { useQuery } from 'react-query';
import api from './api';

const fetchData = async () => {
  const response = await api.getData();
  return response.data;
};

const App = () => {
  const { data, error, isLoading } = useQuery('key', fetchData);

  if (isLoading) {
    return <p>Loading...</p>;
  }

  if (error) {
    return <p>Error: {error.message}</p>;
  }

  return (
    <div>
      <h1>Data:</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

export default App;

 
코드의 양이 엄청나게 짧아진게 보이시나요? 자연스럽게 가독성도 향상되었습니다. 지금은 API를 호출하여 데이터를 가져오는 부분만 확인하였는데 이제 아래에서 자세히 확인해 보겠습니다.
 
 

Reaact Query의 Query 요청

// 가장 기본적인 형태의 React Query useQuery Hook 사용 예시
const { data } = useQuery(
  queryKey, // 이 Query 요청에 대한 응답 데이터를 캐시할 때 사용할 Unique Key (required)
  fetchFn, // 이 Query 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
  options, // useQuery에서 사용되는 Option 객체 (optional)
);

 

useQuery Hook으로 수행되는 Query 요청은 GET 요청과 같이 서버에 저장되어 있는 “상태”를 불러와 사용할 때 사용합니다. useQuery Hook은 요청마다 (API마다) 구분되는 **Unique Key (aka. Query Key)**를 필요로 합니다. React Query는 이 Unique Key로 서버 상태 (aka. API Response)를 로컬에 캐시하고 관리합니다.
 
 

React Query의 Mutation 요청

아래는 데이터를 POST, PUT, DELETE 요청이 올 때 사용하는 Mutation의 예제입니다.

function NotificationSwitch({ value }) {
  // mutate 함수를 호출하여 mutationFn 실행
  const { mutate, isLoading } = useMutation(
    (value) => axios.post(URL, { value }), // mutationFn
  );

  return (
    <Switch
      checked={value}
      disabled={isLoading}
      onChange={(checked) => {
        // mutationFn의 파라미터 'value'로 checked 값 전달
        mutate(checked);
      }}
    />
  );
}
// 가장 기본적인 형태의 React Query useMutation Hook 사용 예시
const { mutate } = useMutation(
  mutationFn, // 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수 (required)
  options, // useMutation에서 사용되는 Option 객체 (optional)
);

 

useMutation Hook으로 수행되는 Mutation 요청은 POST, PUT, DELETE 요청과 같이 서버에 Side Effect를 발생시켜 서버의 상태를 변경시킬 때 사용합니다. useMutation Hook의 첫 번째 파라미터는 이 Mutation 요청을 수행하기 위한 Promise를 Return 하는 함수이며, useMutation의 return 값 중 mutate(또는 mutateAsync) 함수를 호출하여 서버에 Side Effect를 발생시킬 수 있습니다.
 
 

React Query 도입 후 달라진 점

1. Bolierplate 코드의 감소

앞에서 언급한 대로, Redux를 사용할 경우 Redux의 기본 원칙 준수를 위한 다양한 Boilerplate 코드들이 필요합니다. 하지만, React Query를 사용하여 비동기 데이터를 관리하므로 이제는 코드의 분량이 훨씬 적어졌습니다. 코드의 분량이 적어졌다는 것은 개발자에게 불필요한 작업이 필요 없어짐을 뜻하기도 하지만, 소스코드의 복잡도를 낮추어 유지보수의 용이성을 높이고 작업 간에 발생할 수 있는 Side Effect나 휴먼 에러를 사전에 더 잘 막을 수 있다는 의미도 갖게 될 것입니다.

2. API 요청 수행을 위한 규격화된 방식 제공

Redux는 비동기 데이터 관리를 위한 라이브러리가 아닙니다. Redux로 비동기 데이터를 관리하기 위해서 개발자들은 Middleware부터 State 구조까지 다양한 부분을 설계하고 구현해야 했습니다. React Query는 React에서 비동기 데이터를 관리하기 위한 라이브러리입니다. React Query는 API 요청 및 상태 관리를 위해 (상당히 잘 만들어진!) 규격화된 방식을 제공합니다.

3. 사용자 경험 향상을 위한 기능 제공

React Query를 사용할 경우 단순한 옵션 부여만으로 Window Focus 이벤트 발생 시 서버 상태 동기화 시나리오를 달성할 수 있습니다. 다루는 API가 많아지고 컴포넌트 구조가 복잡해질수록 이전의 직접 Event Binding 하는 방식보다 유지보수하기 좋은 코드가 될 것입니다. 더 나아가 React Query에서 제공하는 캐싱 외에 Window Focus Refetching, API Caching, API Retry, Optimistic Update, Persist Caching 등 사용자 경험 향상을 위한 다양한 기법들을 손쉽게 프로젝트에 포함시켜 API 요청과 관련된 번잡한 작업 없이 “핵심 로직”에 집중할 수 있습니다.

// quires/useTodosQuery.ts
// API 상태를 불러오기 위한 React Query Custom Hook

// ...전략

const useTodosQuery = () => {
  return useQuery(QUERY_KEY, fetcher, { refetchOnWindowFocus: true });
};

export default useTodosQuery;

 

 
 

References
https://tech.kakaopay.com/post/react-query-1/
[React] 리덕스 총정리 및 예제
[React] axios를 사용하는 이유 (vs fetch)