리액트는 뷰나 앵귤러와 비교했을 때 가장 큰 차이점이 단방향 바인딩이다.
부모 컴포넌트에서 자식 컴포넌트로만 state를 props로 전달할 수 있으며,
자식 컴포넌트에서 부모 컴포넌트로 props를 직접 전달할 수 없다는 것이다.
자식 컴포넌트에서 부모 컴포넌트에 있는 state를 변경하려면 setState를 props로 넘겨줘야 하는데,
이것이 반복되면 Props Drilling이 발생하게 되며 프로젝트 규모가 커질수록 props의 depth가 증가하게 된다.
이렇게 되면, 불필요한 리렌더링이 발생하는 등 비효율적이다.
따라서, 리액트 props로만 state 관리를 하지 않고, 상태관리 라이브러리도 사용할 필요가 있다.
대표적인 상태관리 라이브러리 - Redux, MobX, Recoil
1. Redux
Redux는 가장 사용량이 많은 상태관리 라이브러리이다.
Redux는 데이터의 흐름이 단방향으로 흐르는 Flux 아키텍처 구조다.
action, reducer, selector, store를 세팅하는 보일러플레이트 코드로 구성되어 있어
유지보수가 쉽고 확장 및 디버깅에 있어서는 엄청난 장점을 가지고 있지만,
많은 양의 보일러플레이트 코드를 쓴다는 단점이 있다.
Redux와 같은 Flux 아키텍처의 라이브러리는 store에 모든 상태를 저장하는 중앙 집중 방식이다.
store는 외부 요소이기 때문에 리액트 내부 스케줄러에 접근할 수가 없고,
Redux경우 비동기 데이터 처리를 하기 위해서 redux-thunk, redux-saga와 같은 별도의 미들웨어 라이브러리를 추가적으로 사용해야 한다.
- Action: Redux에서는 상태를 업데이트하기 위해 Action이라는 객체를 사용한다. Action은 액션 타입(type)과 선택적인 데이터(payload)를 가지며, 앱에서 어떤 변화가 일어났는지 기술한다.
- Dispatch: 액션을 생성하고, Reducer에 전달하여 상태를 변경하기 위해서는 Dispatch 함수를 사용한다. Dispatch 함수는 액션을 전달하고, 이에 대한 상태 업데이트를 Reducer에 요청한다.
- Reducer: Redux에서 상태를 변경하는 로직은 Reducer 함수 안에 작성된다. 변화를 일으키는 함수로 Action의 결과로 state를 어떤 식으로 바꿀지 구체적으로 정의하는 부분이다. 리듀서가 현재 상태와 전달받은 액션 객체를 파라미터로 받아서 새로운 상태로 반환해준다. 리듀서는 파라미터 외의 값을 의존하면 안되고, 이전 상태는 건드리지 않은 상태로 새로운 상태 객체를 만들어 반환하는 순수 함수여야 한다.
- Store: Redux에서는 앱의 상태를 하나의 객체에 저장하는데, 이 객체를 Store라고 한다. 앱의 모든 상태 정보는 이 Store 객체 안에 저장되며, 앱 내부의 어느 곳에서든 접근할 수 있다. 프로젝트에 리덕스를 적용하기 위해 필요한 것으로 프로젝트에는 단 한 개의 Store만 가지며 상태의 중앙 저장소라고 할 수 있다.
Redux의 특징
- 상태를 전역적으로 관리하기 때문에 어느 컴포넌트에 상태를 둬야할지 고민할 필요가 없게 한다.
- 데이터 흐름을 단방향으로 흐르게 한다.
- 상태 관리에서 불변성 유지가 매우 중요하다. 상태를 읽기전용으로 취급한다. Immutable.js 와 같은 라이브러리가 쓰이기도 한다.
- flux 아키텍처를 따른다. (dispatch 관리를 위해 redux-thunk나 redux-saga와 같은 미들웨어가 필수이다.)
- 개념에 대한 이해가 선행되어야 하고, 여러 라이브러리를 함께 사용하는 경우가 있기 때문에 러닝커브가 높은 편이다.
- 액션을 하나 추가하는데, 작성 필요한 부분이 많고, 컴포넌트와 스토어를 연결하는 필수적인 부분들이 있어 코드량이 많아질 수 있다.
2. MobX
Mobx는 Redux와 비슷한 상태 관리 라이브러리이지만 Redux에 비해 간결하고 깔끔한 구조를 가지고 있다.
가장 큰 특징은 모든 상태 변화가 일어나는 부분을 자동으로 추적해주는 것이다.
- State: Observable State로 관찰 받고 있는 데이터이다. Mobx에서는 해당 State가 관찰하고 있다가 변화가 일어나면 Reactions과 Derivations를 발생시킨다.
- Derivations(Computed values): 기존의 상태가 변화에 따라 계산된(파생된) 값을 의미한다.
- Reactions: Observable State의 변화에 따른 부가적인 변화를 의미하고 값이 바뀜에 따라 해야할 일을 정하는 것이다. Derivation과는 달리 값을 생성하지는 않고, 대체로 I/O와 관련된 작업을 하고, DOM업데이트와 네트워크 요청 등에 관여한다.
- Action: 상태를 변경시키는 모든 것을 의미한다. Mobx에서는 모든 사용자의 액션으로 발생하는 상태 변화들이 전부 Derivations와 Reactions로 처리되도록 한다.
MobX는 Redux와 달리 store의 개수에 제한이 없어 여러개가 될 수 있는데,
이것은 분리가 용이해 편리할 수 있는 반면, 상태 변경 시 다수의 store가 영향을 받을 수 있다.
Redux와 달리 store의 데이터를 action 발행 없이 업데이트할 수 있는데,
이것은 구현은 쉽고 용이하지만, 테스트나 유지보수의 측면에서 문제를 일으킬 수 있다.
따라서, 유지보수 및 확장성을 고려해야하는 프로젝트에서 MobX는 좋지 않은 선택일 수 있다.
하지만 Redux보다 러닝커브가 낮고 보일러플레이트 코드의 양 또한 적기 때문에
프로젝트 규모가 크지 않다면 MobX를 사용하는 것이 좋을 수 있다.
Mobx의 특징
- React에 종속적인 라이브러리가 아니고, Redux와 다르게 store에 제한이 없다.
- observable을 기본적으로 사용하고, 절대적으로 필요한 경우에만 state를 변경한다.
- 객체 지향적이다. 그렇기 때문에 서버 개발자들에게 친숙한 아키텍쳐를 제공할 수 있다. 특히 JAVA Spring과 유사하다.
- JAVA의 annotation과 유사한 decorator를 제공한다. Redux에서 컴포넌트와 state를 연결하기 위해 사용하는 mapStateToProps, mapDispatchToProps 등의 보일러플레이트 코드가 사라지고 데코레이터를 통해 깔끔한 코드 작성이 가능하다. Mobx 스토어와 React 컴포넌트를 연결하는게 @inject 데코레이터 하나로 가능하다.
- 캡슐화가 가능하다. Mobx Config 설정을 통해 State를 오직 메서드를 통해서만 변경할 수 있도록 Private하게 관리 가능하다.
- State의 불변성 유지를 위해 노력하지 않아도 된다. Redux에서는 state의 불변성 유지를 위해 여러 라이브러리를 사용하기도 하는데, Mobx에서는 그러지 않아도 된다.
3. Recoil
Recoil은 Context API 기반으로 구현된 함수형 컴포넌트에서만 사용 가능한 라이브러리이다.
Hooks나 Context API와 같은 React에 내장된 상태 관리 기능을 사용하여 상태관리를 할 수 있지만, 아래와 같은 한계가 존재한다.
- 컴포넌트 상태를 공통된 상위 컴포넌트까지 끌어올려 공유할 수 있지만, 이 과정에서 거대한 트리가 리렌더링 되기도 한다.
- Context는 단일 값만 저장 가능하고, 자체 Consumer를 가지는 여러 값들의 집합을 담는 것은 불가하다.
- 위 특성으로 인해 state가 존재하는 곳 부터 state가 사용되는 곳 까지 코드 분할이 어렵게된다.
Recoil은 React스러움을 유지하며 개선하는 방식의 라이브러리이다.
Recoil은 방향 그래프를 정의하고 리액트 트리에 붙이는데,
그래프의 뿌리(atom)으로 부터 순수 함수(selector)를 거쳐 컴포넌트로 흐른다.
Recoil은 Atom과 Selector로 이루어져 있다.
Atom은 상태의 단위로, 유니크한 키값으로 구분된다.
해당 Atom을 구독하고 있으면, 해당 컴포넌트들만 선택적으로 리렌더링이 된다.
Atom의 상태변화는 순수함수를 통해 일어나는데 이를 Selector라고 한다.
Selector는 비동기 처리 뿐만 아니라 데이터 캐싱 기능도 제공하기 때문에 비동기 데이터를 다루기에 용이하다.
- Atoms: Atoms는 Recoil에서 상태의 단위를 의미하고, 업데이트와 구독이 가능하다. atom이 업데이트되면 각각의 구독된 컴포넌트는 새로운 값을 반영해서 리렌더링된다. Atoms는 리액트의 로컬 state 대신 사용할 수 있다. 동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다. Atoms에는 고유한 키가 필요하고 이 키는 전역적으로 고유해야 한다. 그리고 react state처럼 디폴트 값도 가진다. 컴포넌트에서 atom을 읽고 쓸 때는 useRecoilState라는 훅을 사용해야한다. 이건 리액트의 useState와 비슷하나, 상태가 컴포넌트간에 공유될 수 있다는 점에서 차이가 있다. 아래 두 컴포넌트의 state는 공유된다.
- Selector: Selector는 atoms나 다른 selectors를 입력으로 받는 순수 함수(pure function) 이다. 상위 atoms이나 selectors가 업데이트 될 경우 하위 selectors도 재 실행된다. 컴포넌트는 atom 뿐만 아니라 selectors를 구독할 수 있고, 구독하고 있는 selectors가 변경되면 구독한 컴포넌트도 리렌더링된다. Selectors는 상태를 기반으로 데이터를 계산하고 최소한의 상태 집합만 atoms에 저장하고, 파생 데이터는 selector에서 계산하면서 불필요한 상태를 만들어내지 않는다. 컴포넌트 관점에서 atoms와 selectors는 동일한 인터페이스이므로 대체 가능하다. 여기서 get속성은 계산될 함수를 의미하고 전달되는 get인자를 통해 atoms와 다른 selectors에 접근 가능하다. 여기서 접근하면 자동으로 종속 관계가 생성되어 참조했던 atoms나 selectors가 업데이트되면 이 함수도 재실행된다. Selectors는 useRecoilValue()를 통해 조회 가능하다. useRecoilState와는 다르게 writable 하지 않고, 반환 값의 조회만 가능하다. 필요하다면 writable한 selector 작성도 가능하다.
Recoil 특징
- 비동기 처리를 기반으로 작성되어 동시성 모드를 제공하기 때문에, Redux와 같이 다른 비동기 처리 라이브러리에 의존할 필요가 없다.
- Concurrent Mode : 흐름이 여러 개가 존재하는 경우이다. 리액트에서 렌더링의 동작 우선순위를 정하여 적절한 때에 렌더링해준다.
- atom -> selector를 거쳐 컴포넌트로 전달되는 하나의 data-flow를 가지고 있어, 복잡하지 않은 상태 구조를 가지고 있다.
- atom과 selector만 알고도 어느정도 구현이 가능하기 때문에 러닝커브가 비교적 낮다고 할 수 있다.
- store와 같은 외부 요인이 아닌 React 내부의 상태를 활용하고 context API를 통해 구현되어있기 때문에 더 리액트에 가까운 라이브러리라고 할 수 있다.
+) Context API
Context API는 리액트가 자체적으로 가지고 있다.
정적인 데이터 위주로 처리하거나 업데이트가 자주 발생하지 않을 때 사용하기 적합하다.
복잡한 업데이트를 처리 할 때에는 비효율적인데
Provider로 감싸진 부분의 업데이트가 되지 않은 state에도 리렌더링이 발생하기 때문이다.
이런 불필요한 리렌더링을 보완하기 위해 Recoil이란 라이브러리가 나왔다.
Redux, Recoil, MobX, Zustand, Jotai 특징 비교
특징 | Redux | Recoil | MobX | Zustand | Jotai |
데이터 흐름 |
단방향 데이터 흐름 패턴을 사용 | 단방향 데이터 흐름 패턴을 사용 | 양방향 데이터 바인딩 패턴을 사용 | 단방향 데이터 흐름 패턴을 사용 | 단방향 데이터 흐름 패턴을 사용 |
데이터 변화 감지 |
액션(Action)을 통해 변화를 감지 | 변화를 감지하는 상태 Atom을 사용 |
오브젝트를 Observer로 사용함 |
React Hooks를 통해 상태 변화를 감지함 | React Hooks를 통해 상태 변화를 감지함 |
비동기 처리 |
Redux-Thunk, Redux-Saga와 같은 미들웨어를 사용함 | 비동기 처리를 위한 별도의 라이브러리가 필요하지 않음 | MobX-React와 같은 미들웨어를 사용함 | 비동기 처리를 위한 별도의 라이브러리가 필요하지 않음 | 비동기 처리를 위한 별도의 라이브러리가 필요하지 않음 |
러닝 커브 |
Redux는 개념이 간결하고 직관적이지만 초기 학습이 어려움 | Recoil은 개념이 간결하고 초기 학습이 상대적으로 용이함 | MobX는 초기 학습이 비교적 용이함 | Zustand은 상태 관리와 관련된 개념이 간결하며 초기 학습이 상대적으로 용이함 | Jotai는 초기 학습이 비교적 용이함 |
커뮤 니티 |
널리 사용되고 활발한 커뮤니티가 있음 | 아직은 상대적으로 새로운 상태 관리 도구이기 때문에 커뮤니티는 활발하지 않음 | 커뮤니티가 활발하지만 Redux보다는 작음 | 커뮤니티는 활발하지만 Redux보다는 작음 | 커뮤니티는 활발하지만 Redux보다는 작음 |
Redux, Recoil, MobX, Zustand, Jotai의 기능 비교
기능/도구 | Redux | Recoil | MobX | Zustand | Jotai |
코드 크기 | 큼 | 중간 | 작음 | 중간 | 작음 |
비동기 처리 | 미들웨어 필요 | 필요 없음 | 미들웨어 필요 | 필요 없음 | 필요 없음 |
데이터 흐름 | 단방향 | 단방향 | 양방향 | 단방향 | 단방향 |
초기 학습 용이 | 어려움 | 쉬움 | 쉬움 | 쉬움 | 쉬움 |
코드 간결성 | 중간 | 높음 | 높음 | 높음 | 높음 |
유지 보수성 | 높음 | 높음 | 중간 | 높음 | 높음 |
상태 구성 | 객체 | Atom | 객체 | 객체 | Atom |
참고
https://lunit.gitbook.io/redux-in-korean/
https://ykss.netlify.app/react/redux_mobx_recoil/
'기술 개발 > React' 카테고리의 다른 글
React Hooks (useReducer) (0) | 2023.01.05 |
---|---|
컴포넌트의 라이프사이클 메서드 (0) | 2023.01.04 |
React에서 상태관리를 왜 할까? (0) | 2022.10.18 |
리액트를 사용하는 이유 (0) | 2022.10.13 |
About Redux (0) | 2022.08.05 |