항상 새로운 프로젝트를 시작하면서 React의 전역 상태관리를 무엇으로 할 것인가 고민이 많았다.
그 때 마다 redux를 사용해 상태관리의 기본개념과 패턴에 대한 내공을 쌓아보고 싶었지만, 바쁜 일정 때문에 익숙한 zustand를 사용해 빠르게 프로젝트를 진행할 수 밖에 없었다.
하지만 오늘 포스팅을 통해서 redux에 대해서 깊이 있게 정리하고, 새로 시작하는 프로젝트에 적용하는 시간을 가져보려고 한다.
React에 대해서 이야기 할 때, '상태'라는 말을 정말 많이 하게 되는데, 상태는 무엇이고, 이걸 관리한다는 말은 무엇일까? 라는 의문이 들기 시작했다.
나만의 관점에서 정리한다면 상태 == 데이터라고 단순하게 생각한다. 모든 의미를 내포하기엔 어려운 표현이지만 결국 데이터의 변화에 따라 반응하는 리액트의 설계 상 상태가 가진 가장 중요한 의미는 데이터가 아닐까 생각한다.
그렇다면 상태를 관리한다는 것은 무엇일까?
프로젝트를 진행할 때 정말 많은 데이터가 오고 간다. 이 때 예측 가능한 범위 내에서 데이터의 변경이나 호출을 관리하지 않으면 에러가 났을 때, 어디에서 버그가 발생한 것인지 찾기 어려워 디버깅에 어려움을 겪을 수 있다. 이에 따라 많은 개발자들이 다양한 상태관리 방법들을 제시했고, 그중 하나가 redux이다. (사실이 포스팅을 준비하기 전까지는 redux가 리액트에 의존하고 있는 줄 알았지만 상태관리라는 것은 모든 app 내에 필요한 컨셉이라는 것을 알았다.)
상태관리에 사용되는 여러가지 패턴과 전략
Pub/Sub: 느슨한 결합이 필요한 경우
Observable: 복잡한 비동기 데이터 스트림 처리
CQRS: 읽기와 쓰기 작업의 분리가 필요한 경우
Actor: 동시성 처리가 중요한 경우
Event Sourcing: 상태 변화의 이력 추적이 필요한 경우
Atomic State: 세분화된 상태 관리가 필요한 경우
Proxy State: 자동 반응형 상태가 필요한 경우
State Machine: 명확한 상태 전이 규칙이 필요한 경우

Redux는 단방향 데이터 흐름을 가지고 상태 변화의 복잡성을 줄이는 것을 컨셉으로 만들어졌다. 특히 데이터의 흐름을 보았을 때 react팀이 단방향 흐름에 대해 강조하며 채택한 flux 패턴의 흐름을 redux도 가지고 있다고 볼 수 있다. 위에 보이는 사진은 flux 패턴의 흐름도로 맨 처음 소개한 redux의 데이터 흐름도가 이 흐름과 비슷한 흐름을 보니는 것을 확인할 수 있다.
이런 데이터 흐름에는 정답이 있는 것은 아니고 각각 장단점이 공존한다고 생각한다. 단방향 데이터 흐름은 양방향에 비해, 예측가능함에 더 큰 가치를 두고 채택하는 것이 아닐까 싶다. 그러나 단점으로는 변화를 감지하고 화면을 업데이트 하는 코드를 매번 작성해야한다는 불편함이 있다.
앞서 언급했드시 React는 내부적으로 단방향 바인딩을 채택하고 있고, Redux 또한 단방향의 데이터 흐름을 가지고 있다고 했다. 그렇다면 어떻게 react의 전역상태관리 도구로써 Redux를 사용할 수 있을까?
React가 기본적으로 제공하는 api인 contextAPI를 사용해보았다면 provider가 무엇인지 알 것이다. 이는 App 컴포넌트 외부에 생성된 context로 구성된 provider를 감싸는 방식으로 사용해 하위 컴포넌트에서 useContext로 호출해 전역으로 상태를 사용할 수 있는 하나의 방법이다.
(이를 가능하게 하는 것은 React Fiber 덕분인데 이에 대한 내용도 추후에 다뤄보고 싶다.)
Reducer도 완전히 동일하지는 않지만 provider 패턴을 사용해 상태를 전역에 관리하고 있는 store에 접근하는 방식으로 Redux를 react 내에 전역상태관리 도구로 사용이 가능한 것이다.
Redux는 굉장히 '선언적'인 성격을 가지고 있다.
위의 사진을 보면 redux는 store, reducer, dispatch, action으로 구성된 것을 볼 수 있는데,
action을 통해 작동할 동작의 이름과 데이터를 선언해놓고 dispatch를 통해 reducer에 전달하면 action을 읽고 해당 작업을 수행해 store 상태를 변경하는 방식으로 작동한다.
각각에 대해 알아보자!
Store는 Redux의 단 하나의 데이터 원천으로 관리할 데이터를 집약하고 있는 저장소라고 볼 수 있다. 하나의 저장소에 데이터를 모아서 관리함으로써 디버깅을 용이하게 하고, 복잡성을 낮추는 기능을 한다.
Action은 type을 Reducer에 작성된 어떤 로직을 수행할 지 전달하고, payload를 통해 상태 업데이트에 필요한 데이터를 담고 있다. 이를 때때마다 다르게 설정해 놓는게 아니라 미리 선언해놓고 정해진 aciton안에서 dispatch를 통해 reducer에 전달하므로 굉장히 선언적인 프로그래밍이라고 할 수 있다.
const addTodo = (text) => ({
type: ADD_TODO,
payload: text
});
action과 reducer를 연결하는 역할을 한다. 이는 상태변경을 트리거하는 유일한 방법으로써 직접 Store의 상태에 접근하지 못하게 하기 때문에 안정적인 상태관리를 위해 사용되는 방법이다.
store.dispatch(addTodo('학습하기'));
상태를 변경하기 위한 핵심로직을 담당하는 부분이다.
dispatch를 통해 전달된 action의 type을 분기하여 작성된 비즈니스 로직을 수행하는 역할을 한다.
특징으로는 순수함수를 유지해야한다는 점이다. 예측 가능한 범위 안에서 움직이는 것이 중요하므로, 사이드 이펙트가 발생해서 생각지 못한 반응이 일어나선 안되기 때문이다.
const todoReducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [...state, action.payload];
default:
return state;
}
};
이렇게 Reducer의 시작부터 사용까지 왜 사용해야하는지? Reducer의 특징과 작동 방식에 대해서 알아보았다. 이번 포스팅을 통해서 Reducer를 이해하고 사용할 수 있게 되었고, 추가적으로 동기적으로 처리되는 Dispatch의 방식에 대해서 비동기적으로 처리하는 방식에 대한 미들웨어 사용에 대한 궁금증도 생겼다.
추가로 Zustand를 자주 사용했었고, Zustand가 Redux를 기반으로 만들어진 것으로 알고 있는데, 서로 차이점이 뭐고 Zustand는 어떻게 Redux의 보일러플레이트를 줄일 수 있었는지, 속도는 누가 더 빠른지 등에 대해서 궁금증이 생겼다.
또한 이러한 단방향 패턴의 상태관리만 존재하는 것이 아닌 atomic state 패턴을 사용한 jotai, recoil 등에 대한 차이점도 분석해보고 싶어졌다.