계기
React를 공부하던 중 Redux를 자연스럽게 접하게 되었는데, 낯선 내용을 잘 정리하고자 이해한 내용을 녹여낸다.
시간이 걸리더라도 일단은 공식 문서를 보면서 공부해보고자 한다.
Redux란 무엇인가
Redux is a pattern and library for managing and updating global application state,
where the UI triggers events called “actions” to describe what happened, and separate update logic called “reducers” updates the state in response.
Redux란 어플리케이션의 전역 상태를 관리하고, 업데이트 하기 위한 패턴 및 라이브러리이다.
UI가 이른 바 actions로 불리는 이벤트를 발생시키고, reducer라고 불리는 업데이트 로직이 응답으로써 상태를 업데이트 하는 방식으로 작동한다.
어플리케이션 전체에서 사용되는 상태를 관리하기 위해 상태가 예측 가능한 방식으로 업데이트가 되도록 보장하는 방식의 규칙을 적용한 중앙 저장소를 사용한다.
왜 써야 하는가?
The patterns and tools provided by Redux make it easier to understand when, where, why, and how the state in your application is being updated, and how your application logic will behave when those changes occur.
결론적으로 상태의 관리를 원활하게 하여 상태가 언제, 어디서, 왜 바뀌는지를 알기 쉽게 하며,
변경이 발생될 때 프로그램이 어떻게 돌아가는지를 파악하기 쉽게 한다.
상태관리
흔히 React를 개발하게 되면 아래와 같은 3가지가 존재한다.
- State : 어플리케이션을 동작을 결정하는 유일한 기준점
- View : 현재의 상태를 기반으로 한 UI의 선언적 설명 ( 선언형 UI 작업 )
Actions : 사용자 입력에 의해 발생하는 이벤트이며, 상태를 업데이트 하도록 유발하는 것
One Way Data Flow
function Counter() { // State: a counter value const [counter, setCounter] = useState(0) // Action: code that causes an update to the state when something happens const increment = () => { setCounter(prevCounter => prevCounter + 1) } // View: the UI definition return ( <div> Value: {counter} <button onClick={increment}>Increment</button> </div> ) }UI는 State를 기반으로 렌더링되며 버튼 클릭과 같은 일이 일어나는 등 발생한 일에 따라 상태가 업데이트 되며 UI는 새로운 상태를 기반으로 다시 렌더링 된다.
예시 코드를 기준으로는 0이라는 초기 상태를 기준으로 우선 UI가 구성되고 버튼을 누름으로써
onClick이란 이벤트가 발발하여increment라는 액션이 수행되고, 이 액션이 수행되어 state가 바뀌어 이를 바뀐state를 기준으로 UI가 리렌더링 된다.
View를 통해 Action이 발생하고, Action이 State를 변경시키며, State를 통해 UI가 재구성되는 흐름하지만 여러 컴포넌트들이 같은 상태들을 공유할 때, 이를 공유하기 위해 부모의 인자로 상태를 전달하는 방식은 계층이 복잡해질수록 불편함을 야기한다.
이를 해결하기 위해서는 컴포넌트에서 벗어난, 중앙에서 컴포넌트들 간에 상태를 공유하는 것이다. 이를 통해 어떤 컴포넌트에서든 해당 상태에 접근할 수 있으며 이것이 Redux의 근본이다.
불변성
불변성은 변하지 않는 것이다.
Redux는 불변성을 지켜야 한다.
const obj = { a: 1, b: 2 } 이런 식으로 선언하였을 때 겉으로 보기엔 const라 불변하는 것처럼 보이겠지만 obj.b = 3과 같이 내부의 값을 다른 값으로 수정할 수 있다.
Redux에서는 이런 식으로 내부 값을 변경하여 수정하면 안된다.
값을 수정하기 위해서는, 기존의 값을 복사 후 값을 수정한 새로운 객체, 배열로 교체하는 식으로 진행하게 된다.
이를 통해 객체 혹은 배열이 변하지 않음을 보장한다.
React 용어
Actions
action은 type 속성을 갖는 순수한 JS 객체이며 이를 무엇이 일어날지를 서술한 이벤트로서 생각해도 된다.type속성은 액션에 붙일 구체적인 이름 같은 것이다.흔히
"todos/todoAdded"와 같이 /의 앞부분은 이 액션이 속하는 기능이나 카테고리를 의미하며, 뒷 부분은 무엇이 일어날지를 구체적으로 명시하는 것이다.
domain/eventName꼴로 명명한다.또한 추가적인 정보를 다른 속성에 담을 수 있는데, 관례적으로 payload라는 속성에 작성한다.
const addTodoAction = { type: 'todos/todoAdded', payload: 'Buy milk' }Action Creators
action creator는 앞서 설명한action객체를 생성 및 반환하는 함수이다.이를 사용하기 때문에 매번
action객체를 만들 필요는 없다const addTodo = text => { return { type: 'todos/todoAdded', payload: text } }Reducers
Reducer는
현재의 상태와액션을 받아서 상태를 어떻게 업데이트 할 것인지를 결정하고, 새로운 상태를 반환한다.
(state, action) => newState꼴Reducer를
action에 따른 이벤트를 관리하는 이벤트 리스너로 생각해도 좋다.Reducer가 반드시 따라야 할 규칙
- 새로운 상태는 오직 state와 action을 기반으로 결정되어야 한다.
- 현재의 상태를 수정하는 것이 아니라, 복사본에서 필요에 따라 값을 수정한 것을 반환하거나 수정이 필요 없을 경우 받은 상태를 그대로 반환함으로써 불변성을 보장한 업데이트를 진행한다.
- 순수해야 한다 => 비동기적인 로직이나, 수행할 떄마다 다른 결과가 나오거나, 기타 사이드 이펙트를 발생시키면 안된다
아래는 Reducer의 간단한 예시이다.
const initialState = { value: 0 } function counterReducer(state = initialState, action) { action의 종류에 따라 분기처리를 진행하고 if (action.type === 'counter/increment') { // 만약 증가하는 액션이라면 return { // 현재상태를 복사한 객체에서 ...state, // value 속성을 1만큼 더 키운 새로운 객체로 만들어 반환한다. value: state.value + 1 } } // 상태 변화가 필요 없을 경우 현재 상태를 그대로 반환한다. return state }Store
Redux에서
state들이 들어있는 객체를store라고 한다.Store 객체는
Reducer를 넣어서 생성되며,getState를 통해Store에 있는 현재state의 값을 얻을 수 있다.configureStore를 통해Store객체를 생성한다.import { configureStore } from '@reduxjs/toolkit' const store = configureStore({ reducer: counterReducer }) // reducer가 여러개가 있을 경우 아래와 같이 할 수도 있다 const store = configureStore({ reducer: { counter: counterReducer } })Dispatch
Redux의 Store에는
dispatch라는 함수가 존재한다.이것은 상태를 업데이트 하는 유일한 방법이다.
dispatch함수의 인자로action객체를 넣어줌으로써, 해당action에 따라Store객체를 생성할 때 넣어주었던Reducer함수가 동작하고 새로운 상태를 반영한다.Dispatch를 이벤트를 발생시키는 것으로 생각해도 좋다.Reducer가 이벤트 리스너였다면,Dispatch는 이벤트를 발생시키는 것이다.Dispatch가 발생시킨 이벤트를Reducer가 수신하고 업데이트된 state를 반영한다.// 아래처럼 직접 action 객체를 작성해도 되고 store.dispatch({ type: 'counter/increment' }) // 이처럼 액션 객체를 반환하는 함수를 작성하고, 이를 실행시킨 것을 dispatch의 인자로 넘겨주어도 된다. const increment = () => { return { type: 'counter/increment' } } store.dispatch(increment())Selectors
Selector란Store에 저장되어 있는State에서 특정한 정보들을 추출하는 함수이다.앱이 커짐에 따라 Store에는 여러 상태들이 존재할 수 있는데, 이 중 특정 정보를 얻기 위해서 store에서 상태 정보를 얻어올 때마다 특정 정보만을 얻기 위한 불필요하게 반복되는 처리 등을 피할 수 있게 한다.
const selectCounterValue = state => state.value // selectorCounterValue라는 value 값만 가져오는 함수를 만들고, // selector의 인자로 store의 getState를 넣어 value라는 상태만 가져올 수 있도록 한다. const currentValue = selectCounterValue(store.getState())