Redux-Saga에서 React-Query 전환기

Sep 06, 2023
Redux-Saga에서 React-Query 전환기
 
안녕하세요. MyDATA 에서 FE파트 개발하고있는 JT입니다.
이번 글에서는 최근 진행한 프로젝트에서 상태 관리 라이브러리를 Redux-Saga에서 React Query로 전환하면서 얻은 인사이트와 느낀점을 공유하려 합니다.
 
 

1. 왜 상태 관리 라이브러리를 사용하는가?

 
애플리케이션을 개발하면서 다양한 상황과 요구 사항에 대응하기 위해 상태 관리 라이브러리(State Management Library)가 필요하게 됩니다.
저희 프로젝트에서 상태 관리 라이브러리를 도입한 주된 동기는 다음과 같습니다.
  1. 전역 상태 관리
      • 통합된 상태 관리: 여러 컴포넌트와 페이지 간에 공유되는 상태는 중앙 집중식으로 관리됨으로써, 데이터의 일관성 및 신뢰성을 확보할 수 있습니다.
      • 모듈화 및 재사용성: 상태와 그에 따른 로직을 모듈화함으로써, 코드의 구조화가 향상되고 여러 부분에서 재사용이 가능해집니다. 이로 인해 코드의 유지 보수성도 크게 향상됩니다.
      • 디버깅: 상태 관리 라이브러리를 사용하면, 상태 변화를 명확하게 추적할 수 있어 디버깅이나 추적이 용이해집니다.
  1. 비동기 처리
      • 로직의 복잡성 관리: 조건부 호출이나 API 연쇄 호출, 그리고 다양한 에러 핸들링 시나리오 등 복잡한 비동기 로직을 명확하게 관리할 수 있습니다.
      • 캐싱과 최적화: 중복된 API 요청을 피하기 위해 캐싱 기능을 활용함으로써, 네트워크 트래픽을 줄이고 사용자 경험을 향상시킬 수 있습니다. 또한, 코드의 중복을 줄이면서도 재사용성을 높여 개발의 효율성을 증대시킵니다.
 
이 중에서 비동기 처리를 위한 상태관리 라이브러리로서 사용하던 Redux-Saga를 React Query로 전환한 경험에 대해서 본격적으로 이야기해보겠습니다.
 

2. React Query 도입 배경

2-1. Redux-Saga의 복잡함

 
  1. 복잡성
    1. 프로젝트 성격이 한 페이지에서 다수의 API를 호출하는 경우가 잦았고, 특정 조건 하에서 혹은 순차적으로 호출해야 하는 상황이 빈번하게 발생했습니다.
      프로젝트를 진행하면서 추가되는 요구사항들로 인해 API요청이 점점 증가하게 되었고, 이에 따라 각 요청마다 평균 3개의 액션 타입과 액션을 생성해야 했습니다. 이로 인해 프로젝트의 구조가 점점 더 복잡해지는 것을 체감했습니다.
  1. 개발 생산성 저하
    1. Redux-Saga를 활용하여 API를 호출하기 위해서는 여러 단계의 초기 설정과정이 요구됩니다: 액션 타입을 정의하고, 액션 생성자를 만들고, 리듀서를 구현한 후, 제너레이터 함수를 작성하고, 마지막으로 스토어에 등록해야 합니다. 또한, 실제로 비동기 로직을 구현하는 도중에도 액션을 적절하게 디스패치하는 것이 필요합니다. 이런 복잡한 과정 때문에 비동기 로직의 전체적인 흐름을 이해하고 추적하는 것이 어려워져, 결국 개발 속도에 영향을 주게 되었습니다.
       

2-2. React Query 선정 이유

Redux-Saga의 불편함을 겪으면서, 대체할 수 있는 다른 상태 관리 라이브러리에 대한 탐색을 시작하게 되었습니다. 이 과정에서 React Query, RTK-Query, SWR과 같이 뜨고있는 라이브러리들을 주목하게 되었습니다.
 
그 중 React Query를 선정하게된 이유는 아래와 같습니다.
  1. 코드의 간결성
    1. Redux-Saga를 사용할 때보다 React Query를 사용함으로써 비동기 처리 코드가 훨씬 간결해졌습니다. 이로 인해 전반적인 애플리케이션 구조의 복잡도가 감소하고, 개발자들의 생산성이 크게 향상을 기대할 수 있었습니다.
  1. 자동화된 캐싱 기능
    1. React Query는 데이터의 변화 없이 화면이 변경될 때, 불필요한 네트워크 요청을 자동으로 방지하는 캐싱 메커니즘을 가지고 있습니다. 이로써 애플리케이션의 효율성과 사용자 경험을 향상을 기대할 수 있었습니다.
  1. 다양한 이벤트 훅 제공
    1. React Query는 다양한 생명 주기와 상태 변화에 따른 이벤트 훅을 제공합니다. 요청의 성공, 실패, 및 기타 상태 변화에 따라 로직을 쉽게 통합할 수 있었고, 이는 비동기 작업을 더욱 견고하게 만들어주었습니다.
  1. 지원 및 커뮤니티
    1. 공식문서와 커뮤니티에서의 활발한 토론과 기여, 그리고 라이브러리의 지속적인 업데이트는 선택의 중요한 기준 중 하나였습니다.
       
이러한 이유들로 인해 React Query는 저희 프로젝트의 비동기 처리를 담당하는 라이브러리로 선정하게 되었습니다.
 

3. React Query 전환

 
  1. 라이브러리 설치 및 설정
    1. 먼저, React Query를 프로젝트에 설치하고 필요한 설정을 해야합니다.
      npm install react-query
      다음으로는 QueryClient를 초기화하고 기본옵션을 설정 후 QueryClientProvider를 사용하여 앱을 래핑하여 초기화한 queryClient 인스턴스를 전달해 줘야합니다.
      const queryClient = new QueryClient({ defaultOptions: { queries: { retry: 0, //재시도 횟수를 지정합니다. Suspense: true, //React Suspense를 사용하여 데이터 로딩 상태를 처리하도록 설정합니다. useErrorBoundary: true, //에러 발생 시 해당 에러를 상위의 에러 경계(Error Boundary) 컴포넌트로 전달하도록 설정합니다. }, mutations: { Suspense: true, useErrorBoundary: true, }, }, })
      function App() { return ( <QueryClientProvider client={queryClient}> {/* 나머지 앱 컴포넌트들 */} </QueryClientProvider> ); }
 
  1. useQuery 도입
    1. 각 페이지 혹은 컴포넌트에서 데이터 패칭을 위해 사용되었던 비동기 로직을 useQuery로 전환하였습니다.
      // 기존 Redux-Saga 호출방법 function* fetchData(action) { try { const data = yield call(api.getData, action.payload); yield put(fetchSuccess(data)); } catch (error) { yield put(fetchFailure(error)); } }
      위와 같이 처리했던 기존의 Redux-Saga 비동기 처리 로직을 아래와 같이 useQuery 훅을 사용하여 수정해 주었습니다.
      function Component() { const { data, error, isLoading } = useQuery('dataKey', () => api.getData()); }
       
  1. useMutation 도입
    1. useMutation 훅은 데이터 변경, 즉 POST, PUT, DELETE와 같은 작업을 수행할 때 활용할 수 있습니다. 앞서 말한대로 redux-saga에서는 다양한 부분을 관리해야하는 수고를 덜어 아래와 같이 간소화 할 수 있습니다.
      function Component() { const mutation = useMutation(newData => axios.post('/data-endpoint', newData) );
       
  1. 캐싱 활용 및 Query Key 적용
    1. React Query에서는 자동으로 캐싱이 적용되기 때문에, 이를 활용하기 위해서는 Query Key가 중복되는 것을 방지하여 휴먼 에러를 막아야합니다.
       
  1. 기존 saga 코드 제거
    1. 기존에 사용되었던 saga 코드를 제거하여
       

4. React Query 옵션 활용

 
React Query를 적용한 후, 사용한 몇가지 옵션과 사례에 대해서 경험을 공유하려합니다.
  1. onSuccess, onError
    1. 쿼리의 결과나 오류에 따라 호출될 콜백 함수를 지정합니다.
      데이터를 성공적으로 가져왔을 때 특정 로직을 실행하거나, 오류 발생 시 사용자에게 알림을 보내는 등의 작업을 손쉽게 처리할 수 있습니다.
      useQuery(['post', postId], fetchPost, { onSuccess: (data) => { console.log("Data fetched successfully!", data); }, onError: (error) => { alert("An error occurred!"); } });
       
  1. staleTime
    1. 특정 쿼리의 데이터가 stale해지기 전까지의 시간(ms)을 지정합니다.
      이 옵션을 사용하면, 데이터를 너무 자주 재조회하지 않도록 할 수 있습니다.
      예를 들어, 유저의 프로필 정보와 같은 변경 빈도가 낮거나 로그아웃 전까지는 변경 되지 않는 정보에 대해서 설정할 수 있습니다.
      useQuery(['user', userId], fetchUser, { staleTime: Infinity });
       
  1. enabled
    1. 해당 쿼리의 실행 여부를 제어합니다. 이 값이 false로 설정되면 쿼리는 자동으로 실행되지 않고, true일 경우 쿼리가 활성화되어 실행됩니다.
      기본적으로 이 옵션은 true이기 때문에 사용자가 다른 탭으로 이동했다가 다시 돌아오면 자동으로 데이터를 새로 가져옵니다. 그래서 특정 조건에 한해서 비동기 처리를 하게 설정할 때 사용합니다.
      예를들어, 유저 프로필 정보를 수정하는 페이지에서 데이터 패칭할때, 다음과 같이 userId가 존재하는 조건을 넣을 수 있습니다.
      useQuery(['user', userId], fetchUserById, { enabled: userId });
 
React Query의 다양한 옵션들은 비동기 데이터를 처리하는 데 필요한 세밀한 조절을 가능하게 합니다. 이로써 개발자는 데이터의 신선도, 캐시 지속 시간, 그리고 데이터 요청 성공 및 실패에 따른 로직 처리 등을 효과적으로 관리할 수 있습니다.
 
해당 공식문서에서 여러 기능과 옵션 설정을 확인 할 수 있습니다.
 

5. 마무리

 
Redux-Saga에서 React Query로 전환하면서 코드의 간결화, 생산성 향상 등의 여러 이점을 경험하였습니다. 특히, 이벤트 훅과 같은 React Query의 기능들에 대한 사용 경험을 공유하였습니다.
프로젝트의 특성, 개발 팀의 경험, 그리고 비즈니스 요구사항을 종합적으로 고려하여 적합한 라이브러리를 선택하는 것이 중요하다는 것을 다시 한번 느꼈습니다. 이 글이 React Query의 도입을 고려 중인 다른 팀이나 개발자들에게 유용한 정보가 되길 바랍니다.
감사합니다.
 
Share article

MYDATA Tech Blog 💻