방학동안 작게 진행 할 프로젝트에서 리덕스를 써보고 싶어서 리덕스를 공부하기로 했다.
react의 context api 를 사용할 수도 있고, 리액트 팀에서 만든 리코일이 핫하게 떠오르고 있다고 하지만 다음과 같은 이유로 리덕스를 사용 해 보기로 했다.
- 상태 관리 라이브러리 경험을 위해서
내가 진행할 프로젝트에서는 상태관리를 복잡하게 할 필요는 없을 것 같다. 그러나 리액트 상태 관리에 대해 검색해본 바 현업에서는 대부분 상태 관리 라이브러리를 따로 사용하는 것 같았다. 따라서 상태 관리 라이브러리를 사용 해 보아야 겠다고 생각했다. - context의 한계
context가 있음에도 상태 관리 라이브러리를 사용하는 이유는 다음 글에서 알아 볼 수 있다.
https://yrnana.dev/post/2021-08-21-context-api-redux
context의 state가 object일 때, object의 상태가 부분적으로 변경되더라도 모든 컴포넌트가 리렌더링이 된다. 특정 작은 부분만의 변화가 있을때만 리렌더링 하고 싶어도 그럴 수 없다.
또한 컨텍스트를 추가할 때 마다 프로바이더를 중첩해야하는 번거로움이 있다.
따라서 context api는 글로벌 상태관리 라이브러리를 완벽히 대체하기 어려움이 있다. - 아직은 그래도 Redux의 점유율이 높다.
페이스북에서 아예 리액트 친화적이라고 내놓고 평도 좋은 리코일도 사용해 보고 싶은 마음이 크지만.. 아직은 리코일의 점유율이 너무 작아보인다. 방학을 상태 관리에만 모두 쏟을 수는 없기 때문에 사용자가 많고 레퍼런스가 많은 리덕스를 우선적으로 선택하기로 했다. - 편리해 보이는 리덕스 개발자 익스텐션
- 2학기의 프로젝트에서도 리덕스를 사용할 확률이 높아보이므로 미리 예습!
공식사이트를 보며 배운 내용들을 정리하는 글
리덕스란 ?
리덕스는 'action'이라는 이벤트를 불러서 애플리케이션의 상태를 관리하고 업데이트하기 위한 라이브러리이다. 전체 애플리케이션에서 필요한 상태들을 저장하는 중앙 저장소 역할을 한다.
Redux 용어 정리
Store
store는 모든 리덕스 애플리케이션의 중심이다. store는 애플리케이션의 전역 상태를 담는 컨테이너다.
리듀서를 전달하여 생성하고, 스테이트를 리턴하는 getState 함수를 갖고 있다.
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
스토어는 특별한 함수와 능력을 갖고 있는 js 객체 이다. ( 일반 js객체와는 다름)
- 리덕스 store의 내부를 유지하기 위해서 직접적으로 state를 조작하거나 변경하면 안된다.
- state를 업데이트 하는 유일한 방법은 action을 생성 하고 그것을 store에 dispatch 하는 것.
- action이 dispatch되면 스토어는 reducer 함수를 실행하고 그것이 기존의 state와 새로운 action을 비교해서 새로운 state를 결정한다.
action은 type필드를 가진 순수 자바스크립트 오브젝트이다.
리덕스는 action이 dispatch 되면 루트 Reducer를 항상 실행한다.
Action
action은 type 필드를 가지는 순수 자바스크립트 객체이다. 애플리케이션에 어떤 일이 일어났을 때 발생하는 이벤트라고 생각할 수 있다.
type필드는 반드시 string으로 주어져야 하고, 이 동작을 서술하도록 한다. 예) “todos/todoAdded”
보통은 다음과 같은 규칙으로 작성한다. “domain/eventName”
domain : 이 액션이 속한 feature 혹은 카테고리
eventName : 구체적으로 발생한 이벤트 명
추가적인 설명을 기입하는 payload 필드도 가질 수 있다.
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
Reducer
Reducer는 현재 state와 action을 인자로 받는 함수로써 현재 state와 action을 통해 새로운 state를 계산하고, state가 변경 되었으면 state를 새로운 state로 업데이트 하게 결정한다.
Reducer는 새로운 스테이트를 리턴한다: (state, action) => newState
action이 일어날 때 이것을 처리하는 이벤트 리스너라고 생각할 수 있다.
Reducer는 다음과 같은 규칙들을 따라야 한다.
- state와 action 인자들에 의해서만 새로운 state를 계산해야 한다.
- 현재 존재하는 state를 조작하면 안된다. 그 대신 현재 state를 복사한 배열을 조작하여 immutable updates를 만들어야 한다.
- 비동기적인 로직을 처리해선 안된다.
- Reducer를 작성하는 전형적인 방법
- 액션이 발생하면 해당 액션이 reducer가 처리하는 액션인지 확인한다.
- 만약 처리 한다면, state를 복사해서 새로운 값을 업데이트하고 이것을 리턴한다.
- reducer에서 처리하지 않는 액션이면 현재 (변경되지 않은) state를 리턴한다.
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// Check to see if the reducer cares about this action
if (action.type === 'counter/incremented') {
// If so, make a copy of `state`
return {
...state,
// and update the copy with the new value
value: state.value + 1
}
}
// otherwise return the existing state unchanged
return state
}
Dispatch
리덕스 store는 dispatch라는 함수를 가진다. state를 업데이트 하기 위해서는 무조건 store.dispatch() 를 사용해서 action 객체를 전달해야 한다.
store는 reducer함수를 실행시키고 새 state를 저장할 것이다. 업데이트된 state를 확인하기 위해서 getState()함수를 호출 할 수 있다.
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
dispatch 하는 것을 이벤트를 트리거 한다고 생각할 수 있다. 어플리케이션에서 어떤일이 일어나면 우리는 store가 이것을 알아채도록 해야 한다. reducer 는 이벤트 리스너 처럼 action이 발생하면 state를 업데이트 한다.
Selector
selector는 store의 state를 추출 하는 함수이다. 어플리케이션이 커지면 state를 가져오는 코드를 작성할 일이 많은데 그럴 때 마다 getState()를 중복적으로 작성하는 것을 방지하는 코드이다.
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
Data Flow
리덕스의 데이터 흐름은 크게 다음과 같이 3단계로 구성된다.
- 유저가 클릭같은 활동을 하면 action이 dispatch 된다.
- 스토어는 reducer를 사용하여 새로운 state를 계산한다.
- ui는 새로운 값을 업데이트 하기 위해서 스테이트를 읽는다.
예시
1. Deposit이라는 클릭 이벤트가 발생 한다.
2. 이벤트 핸들러에서 action을 디스패치 한다.
3. 스토어는 Reducer 함수를 이용해서 기존 state와 action을 비교해 state를 업데이트 한다.
이후 ui는 업데이트된 state에 따라 리렌더링 된다.
새로운 state $10으로 업데이트 된 UI