Programming/React & TypeScript

상태관리 도구들의 차이점 정리 [ Redux / Redux-Toolkit / Recoil ] + Redux 미들웨어들 [Redux-Saga / Redux - Thunk ..etc ]

감귤밭호지차 2023. 9. 22. 01:09

* Redux 정리 글 

* Redux-Toolkit 정리 글 

* 기타 상태 관리 도구 정리 글 (useState / useContext / Recoil / Mobix ) 


 

React 에서 사용하고 있는 상태관리 도구들의 각각의 차이점과 장단점 정리 글

 

React는 여러 상태 관리 도구들을 사용할 수 있는데 각기 장단점을 가지고 있어서 이들의 차이점에 대해서 정리해보는 시간을 가지도록 하겠습니다. 참고로 저는 Redux-Toolkit을 사랑하지만 많은 회사에서는 Redux , Redux-Saga를 많이 사용하더라구용.. Recoil 도 꽤 쓰고 말이죵... 

 

 

우선 프로젝트를 진행할 때 왜? 상태관리 도구를 사용하는 이유에 대해서 알아보겠습니다. 

컴포넌트 간의 정보 공유가 필요할 때, React에서 자식 컴포넌트 간의 다이렉트 데이터 전달은 불가능합니다. 그래서 보통은 부모 컴포넌트를 통해서 전달해야 하는 자식 컴포넌트까지 내려가면서 전달을 해야 합니다. 

물론, 자식 컴포넌트가 하나면 금방 전달 할 수 있겠지만 프로젝트 규모가 커지고 자식 컴포넌트들의 수가 너무 많다면 계속 props 를 내려주다가 " props drilling " 이라는 이슈가 발생할 수 있습니다. 이러한 복잡성을 줄이기 위해 탄생한 것이 redux, recoil 등과 같은 상태 관리 라이브러리 도구입니다. 또한 이러한 라이브러리들을 사용함으로써 한 곳에서 전역에서 사용되는 상태를 관리할 수 있다는 편리함도 상태관리 도구를 사용하는 이유 중 하나입니다. 

 

[1] Redux 

대표적인 JS 상태 관리 라이브러리이며 본질은 Node.js 모듈입니다. Redux가 가지고 있는 개념 세 가지로 Redux를 설명할 수 있는데요. 

1. 동일한 데이터는 항상 같은 곳 , 전역에서 상태를 저장하는 하나 뿐인 Store 데이터 저장공간에서 가져온다. 

2. 상태는 읽기 전용이며 Dispatch를 이용해서 Action 객체에 전달해야 업데이트가 가능하다. 

3. 변화는 오직 순수함수 

 

장점 단점
순수 함수를 사용하기 때문에 상태 예측 가능하고 유지 보수가 용이  상태는 읽기 전용이라는 불변성 유지를 위한 코드의 증가

* 이러한 불변성을 유지하기 위해 상태를 수정하는 대신 새로운 상태를 복사하고 수정한 후에 이를 저장해야 하므로 코드가 복잡해지고 성능 문제도 발생할 수 있음 
크롬 확장 프로그램을 이용할 수 있기 때문에 디버깅에 유리

* action 과 state log 기록 시 redux dev tool 에서 확인 가능 
코드 양의 증가 : 보일러프레이트 

* Action 타입, Action 생성자, Reducer 등 다양한 코드를 작성
* 유지 보수 어려워지고 코드 미스가 날 수도 있음
상태 데이터는 Store 데이터 저장 공간에서 관리하는 편리함 컴포넌트와의 연결 경계 생성으로 인한 재사용성 감소

* 컴포넌트와 상태를 연결하기 위한 코드를 작성해야 함 
- connect 함수 혹은 useSelector, useDispatch 
* 이러한 코드 연결은 컴포넌트와 Redux 상태 간의 경계를 만들고 컴포넌트의 재사용성을 감소 시킬 수 있음 

 

 

[2] Redux  - Toolkit

내용

 

[3] MobX 

React에서 사용할 수 있는 상태 관리 라이브러리로 클래스형 컴포넌트를 기준으로 만들어져 객체지향 프로그래밍 코드를 작성할 수 있습니다. 따라서 Mobx와 React의 함수형 컴포넌트의 Hook을 함께 사용하면 오류가 발생할 수 있습니다.

* 물론 mobx-react v6 혹은 mobx-react-lite를 사용하면 Hook을 함께 사용할 수 있습니다. 

 

렌더링 할 State를 관찰대상(observable)으로 지정하고 해당 State 를 변경하면 React Component Render 함수에 의해 렌더링 되는 구조입니다. Mobx 는 store (파일이 여러개 생성 가능 ) 을 전역 객체로 활용해서 각 컴포넌트에서 import 해서 사용하는 방식입니다. 각 store 파일에서 상태를 정의하고 해당 상태를 수저할 수 있는 action 함수를 구현합니다. 참고로 React Context를 사용해서 외부 state를 사용할 수도있습니다. 

 

 

@observable tempMobx = [{
	name: 'emma',
    age: 1996
}]

//바벨로 변환되면서 아래와 같이 자동으로 변함
tempMobx = observable([{
	name: 'emma',
    age: 1996
}])

 

장점 단점
Redux에 비해 코드가 간단하고 가벼움

* Decorator(@) 을 사용하기 때문에 보일러플레이트 제거가능
러닝커브가 있고 데코레이터 패턴과 같은 실험적 문법으로 인해 추가적인 학습이 필요함.
클래스형 컴포넌트를 리턴하기 때문에 객체 지향적

*캡슐화 적용 가능
레퍼런스가 부족
state의 불변성을 신경쓰지 않아도 됨  SSR 설정이 어려움

* 데이터 초기화 및 동기화측면에서 고려해야 할 사항이 많음

 

* 참고 : Mobx 공식문서 튜토리얼 

* 참고 : Mobx 훑어보기

 

 

 

***** PS *****

 

Redux , Mobx는 **Flux 패턴을 기반으로 설계되어 안정적인 전역 상태 관리가 가능했습니다만 아래와 같은 불편한점들이 존재합니다. 

 

 

- React 전용 라이브러리가 아님

     : 외부요인으로 Store가 취급되어 동시성 모드를 구현하기에 호환성이 부족 

- 복잡한 보일러 플레이트 초기 세팅 

     : Store, Action, Reducer 등의 코드 작성이 필요하며 러닝커브가 높음

- 비동기 데이터에 추가 리소스가 필요 

     : 전역 상태에 비동기 데이터를 호출하기 위한 서드파티 라이브러리(Redux-Saga ..etc) 가 필요 

 

 

***** PS *****

[4] Recoil 

페이스북에서 출시한 React 전용 전역 상태 관리 라이브러리입니다. Redux 처럼 (action, reducer 등)을 작성하거나 사용할 필요가 없으며 비동기 요청이 매우 심플 합니다. React 동시성 모드, Suspense 등을 지원하기 때문에 UX 에서도 굉장히 유리합니다. 많은 기능을 제공하기 때문에 비교적 큰 규모의 애플리케이션에서 유용합니다. 

 

atoms ( 공유 상태 ) 에서 selectors( 순수 함수 ) 를 거쳐 React 컴포넌트로 내려가는 data-flow graph 구조로 작동합니다. atom이 업데이트 되면 구독하고 있는 모든 컴포넌트는 새로운 값을 반영하며 리 렌더링됩니다. selectors는 atoms 상태 값을 동기/비동기 방식을 통해 변환할 수 있습니다. 최소한의 상태 집합만 atoms 에 저장하고 다른 파생 데이터는 selectors에 명시한 함수를 통해 효율적으로 계산하여 쓸모없는 상태의 보존을 방지합니다. 어떤 컴포넌트가 자신을 필요하는지 그리고 어떤 상태에 의존하는지 " 추적 " 하기 때문에 이러한 함수적인 접근 방식을 매우 효율적으로 만듭니다. 

 

 

* 동시성 모드 (React Concurrent Mode) : react 의 새로운 기능 중 하나로 애플리케이션의 성능과 사용자 경험을 개선하기 위해 도입된 기능입니다. React의 내부 작동 방식을 변경하고 리액트 앱이 동시에 여러 작업을 수행하고 업데이트 할 수 있게 해줍니다. 

주요 이점으로는 ( 더 빠른 렌더링 / 자연스러운 UX / 에러 처리 개선 / 비동기 렌더링 ) 등이 있습니다. 

 

 

 

장점 단점
비동기 요청이 매우 심플하고 React 전용 라이브러리로 내부 접근성이 용이 실험적인 api 들이 많음
동시성모드, Suspense 등을 지원하여 UX에 굉장히 유리 레퍼런스 부족 및 기능 부족 

* migration 기능이 부재 + 공식 debtools 부재
전역 상태 설정/정의가 매우 간단 하여 러닝커브가 매우 낮음

* recoil 디렉토리만 필요
* Recoil이 지원하는 Hook으로 get/set 하기 때문에 React 문법과 유사
SSR 구현 및 관리가 다소 복잡

 

 

 

* 참고 : 전역 상태관리 라이브러리  - Recoil 정복기

* 참고 : Recoil을 이용한 상태 관리

* 공식 문서 : Recoil 주요 개념 

 

 

 

 

 

 


 

 

 

[5] Context API  

다양한 레벨( 자식 컴포넌트) 에게 데이터를 전달하는 방식으로 props drilling 을 방지하는 것이 주 목적인 React의 내장 함수입니다. 

useContext 를 이용해서 Context 객체를 구독하고 해당 context의 현재 값을 반환하는 방식을 이용해 컴포넌트 간의 데이터를 전달하고 공유할 수 있습니다. 

 

다만 Context 는 root 컴포넌트에 Provider로 갑싸는데 이 상태가 바뀌면 모든 하위 컴포넌트가 함께 리 렌더링이 되는 이슈가 발생하다는 문제가 있습니다. 

 

# React 공식 문서 : Context API

 

장점 단점
러닝커브 낮고 접근성이 높음 Provider의 불필요한 리렌더링으로 인한 성능 저하 

* value 값이 변경될 때 마다 모든 하위 컴포넌트가 리렌더링이 되어 불필요한 성능 저하가 발생할 수 있음 
* 이를 해결하기 위해 context 객체를 (상태와 액션) 분리 하여 리렌더링을 줄이거나 메모이제이션을 이용해서 객체가 변경되지 않는 한 리렌더링을 방지할 수 있다. 
* 혹은 최적화 기술 (React.memo , shouldComponentupdate) 와 같은 기술을 사용해서 리렌더링을 최소화할 수 있다. 
코드가 간결

 

 

 

 

 


 

 

 

Redux를 돕는 MiddleWare 친구들  ( Redux - Saga / Redux - Thunk / Promise / logger ... ) 

Redux의 기능을 확장하기 위한 수단으로 Dispatch 함수를 감싸는 역할을 수행합니다. 여러 미들웨어는 서로 체이닝이 되고 최종적으로 체이닝이 된 함수가 Store의 Dispatch 함수로 주입이 됩니다. 즉, Store와 Action 생성자 사이에서 동작한다고 이해하면 됩니다. 

 

 

원래는 Action 객체 생성시 Dispatch 함수가 이를 Store에 전달하는 흐름이었다면 미들웨어가 중간에 투입이 됨으로써 Action을 대신 받아 전달하거나 api 통신과 같은 다른 미들웨어와의 작업을 수행하고 Store로 전달하기도 합니다. 미들웨어는 여러 개가 있을 수 있으며 Action을 중간에 가로채고 필요한 작업을 수행 한 다음 다음 미들웨어에 전달하거나 Store에 전달 할 수 있습니다. 

 

 

[1 - 01] Redux-Saga 

비동기 작업을 관리하고 부수효과를 처리하기 위한 Redux 미들웨어의 대표적인 종류 중 하나 입니다. (redux-saga는 미들웨어 역할을 하는 라이브러리) 이를 통해 비동기 작업을 수행하고 복잡한 비즈니스 로직을 분리하여 관리하는데 사용됩니다. Generator 함수를 이용해서 Action의 비동기 흐름을 더 자세하게 제어할 수 있습니다. 덕분에 Action 생성자는 순수 함수로 유지될 수 있고 로직은 별도의 Saga에서 처리됩니다. 

npm install redux-saga –-save

 

 

# 특징 

- Generator 함수를 사용해서 Action 을 모니터링하고 비동기 작업을 수행 - Action 생성자는 순수 함수로 유지 : 코드 간결

- API 호출, 데이터 가져오기, 타이머, 비동기 로직 실행 등을 처리할 수 있음   

- 복잡한 비동기 흐름을 다루기에 적합하며 여러 비동기 작업을 조합하고 관리할 수 있음

- 부가적인 기능: redux-saga-effects( 에러 헨들링, 딜레이 등) 을 쉽게 추가할 수 있음

 

# But,, 

- 초기 설정에 비교적 복잡해서 러닝커브 높음

- Generator 함수나 redux-saga-effect와 같은 추가적 코드 작성으로 인해 어려움.

 

 

* redux-saga-effects는 Redux-Saga의 공식 확장 라이브러리 

: API 호출 / 액션을 dispatch 하거나 취소 / 딜레이, 타이머 작업 / 조건문 반복문 사용해서 Saga의 흐름을 제어 하는 작업들 수행

>> 대표적인 예시

(1). call : API 호출

(2). delay : 딜레이 추가 

(etc..) put , takeEvery...

 

더보기

개인적 궁금증으로 - 상태 관리 중간에 API 호출 작업이 필요한 경우가 뭐가 있을까 알아보았따. 

- 데이터 로딩 

- 폼 제출 ( 로그인, 회원가입 , 댓글 작성 ) 

- 실시간 업데이트 (웹 소켓 통신 : 채팅, 실시간 알림, 주식 시세 업데이트 등 ) 

- 상태 업데이트 ( 포스트 좋아요, 상품 찜하기 ) 

- 에러 헨들링 

 - 인증 

 - 외부 데이터 통합 ( 지도 서비스, 날씨 정보, 뉴스 피드 등)

 - 상태 동기화 (브라우저 오프라인 모드에서 작업한 내용을 서버에 동기화 할 경우 ) 

 

이처럼 일반적인 기능들이 있기는 한데 굳이 Redux 미들웨어를 사용해서 코드를 작성해야 하는 것은 아니다. 다만 상태관리와 분리를 통해 코드의 일관성과 유지보수성을 높이고, 비동기 작업들을 좀 더 쉽게 다룰 수 있으며 테스트가 용이하다는 장점들이 있기 때문에 유용하다는 설명이 있다. 따라서 프로젝트의 복잡성에 따라 적절한 방식을 선택하는 것이 중요하다고 하다. 

 

 

* 참고 블로그 : 리덕스 사가란 무엇인가? 

 

 

 

 

[1 - 02] Redux-Thunk 

Redux 창시자인 Dan Abramov 가 만든 가장 많이 사용되는 비동기 작업 미들웨어 입니다.  Action 생성자가 일반 함수 대신 함수를 반환할 수 있도록 도와줍니다. 이 함수는 dispatch와 getState 로 props(인수)를 받아 비동기 작업을 수행하고 Action 객체를  dispatch()를 이용해서 업데이트할 수 있습니다. 

npm install redux-thunk –-save

 

 

# 특징 

- 간단한 비동기 작업에 대한 빠른 설정이 가능

- 매우 간단한 코드로 비동기 작업 수행이 가능 

- 러닝커브 낮음 

- 주로 간단한 API 호출 및 상태 업데이트에 사용

 

# But,, 

- 비동기로직이 Action 생성자에 포함되어 이 부분이 복잡해질 수 있음

- Action 생성자가 너무 많은 역할을 담당해서 코드 관리가 어려워질 수 있음 

 

 

 

 

** Redux Thunk vs Redux Saga ** 

Redux Thunk Redux Saga
Action 생성자에서 비동기 로직을 처리
코드가 복잡해지고 너무 많은 역할을 담당
Action 생성자는 순수함수를 유지
별도의 Saga에서 로직 처리
*코드의 가독성 향상
적은 보일러 플레이트코드를 보유 Redux-Thunk에 비해 많은 보일러 플레이트코드를 보유
코드 확장이 어려움 Redux-Thunk에 비해 코드 확장이 쉬움
비동기 함수 테스트가 어려움 모든 비동기 로직이 함께 유지되므로 테스트하기 쉬움
Redux-Saga에 비해 로직.함수, 개념이 이해하기 쉬움 
* 러닝커브 낮음
Generator 함수나 redux-saga effect와 같은 개념과 같은 여러 개념으로 인해 이해하기 어려움
* 러닝커브 비교적 높음  ( But, 이로인해 효율적인 비동기 관리 가능)

* 보일러 플레이트 : 최소한의 변경으로 여러 곳에서 재사용 되며, 반복적으로 비슷한 형태를 띄는 코드 

* 참고 블로그 : Redux-Saga vs Redux-Thunk : Key Differences With Examples

 

 

 

 

 

[1 - 03] Redux-Promise-Middleware 

Promise 기반의 비동기 작업을 조금 더 편하게 해주는 미들웨어입니다. Payload로 전달된 객체가 만약 Promise 객체라면 통신에 대한 응답이 올 때까지 기다린 이후 결과값을 Reducer에 전달합니다. ( 지연 - 전달 ) 

 

# 특징 

- 비교적 코드가 단순하며 조건부로 작업을 수행해야 하는 경우 유용

- Promise 기반의 비동기 작업 수행 시 해당 미들웨어를 사용하지 않으면 Action 생성함수는 payload 로 Promise 객체 자체를 실어 보내기 때문에 Reducer 에 상태를 정상적으로 전달 할 수 없음. 

npm i redux-promise-middleware

 

 

[1 - 04] Redux-Logger

npm i --save redux-logger