리덕스 코어와 리덕스 툴킷이 존재합니다. 둘의 차이점은 Redux Toolkit은 Redux 코어 뿐만 아니라 Redux Thunk나 Reselect 등의 패키지를 포함한다는 점입니다. 또 리덕스는 의존성 패키지를 포함해 2Kb의 매우 작은 용량입니다.
# Redux Core 설치하기
# NPM
npm install redux
# Yarn
yarn add redux
import { createStore } from 'redux'
/**
* `function Func (state, action) => state` 형태는 순수한 함수인 리듀서입니다.
* 리듀서는 action이 어떻게 state를 다음 state로 변경하는지 서술합니다.
*
* state의 type은 기본형(primitive)일수도, 배열일수도, 객체일수도 있습니다.
* 심지어 Immutable.js 자료구조일수도 있습니다.
* state를 다룰 때 가장 중요한 점은 직접 mutate(변형)해서는 안된다는 점이며,
* state가 바뀐다면 변형이 아닌새로운 객체를 반환해야 한다는 것입니다.
*
* 이 예제에서 우리는 `switch` 구문과 문자열을 썼지만,
* 여러분의 프로젝트에 맞게 `map`과 같은 다른 컨벤션을 따르셔도 좋습니다.
* 또, case의 명칭을 const 상수로 지정하여 human error를 방지하고 최적화할 수 있습니다.
*/
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
// 앱의 state를 보관하는 Redux store를 만듭니다.
// createStore의 API로는 { subscribe, dispatch, getState }가 있습니다.
let store = createStore(counter)
// subscribe()를 이용해 상태 변화에 따라 UI가 변경되게 할 수 있습니다.
// 보통은 subscribe()를 직접 사용하기보다는 뷰 바인딩 라이브러리(예를 들어 React Redux)를 사용합니다.
// 하지만 이는 현재 상태를 localStorage에 영속적으로 저장할 때에 편리합니다.
store.subscribe(() => console.log(store.getState())))
// 내부 상태를 변경하는 유일한 방법은 액션을 보내는 것뿐입니다.
// 액션은 직렬화할수도, 로깅할수도, 저장할수도 있으며 나중에 재실행할수도 있습니다.
store.dispatch({ type: 'INCREMENT' })
// 1
store.dispatch({ type: 'INCREMENT' })
// 2
store.dispatch({ type: 'DECREMENT' })
// 1
규모가 작은 프로젝트에서는 액션 타입을 따로 상수로 선언할 필요는 없습니다. 하지만 여러 사람들과 협업이 이루어지는 큰 프로젝트에서는 액션 타입을 상수로 정의하는 이점들이 있습니다.
앱의 모든 state는 하나의 store 안에 하나의 객체 트리 구조로 저장됩니다.
리덕스는 이를 통해서 다양한 환경에서 실행 가능한 범용적인 앱을 쉽게 만들 수 있습니다.
server로부터 가져온 state는 serialized되거나 수화되어 전달되며 클라이언트에서 추가적인 코딩 없이도 사용할 수 있습니다. 또한 빠른 개발 사이클을 위해 현재 개발중인 앱의 상태를 저장하며 state 객체 트리를 하나만 갖고 있기 때문에 undo, redo를 쉽게 구현할 수 있습니다.
오직 'Action'을 reducer에게 전달하는 방법으로만 상태를 변화시켜야 합니다. 아래 예시처럼 직접 state를 mutate(변형)하지 않아야 합니다. 모든 상태 변화는 reducer등 API를 통해 중앙 store에서 관리되어야 합니다.
// Don't
state_arr = []
state_arr.push([abc])
// Do This!!
const reducer = (state = [], action) => {
switch (action.type) {
case ADD_TODO:
return [...state, { text: action.text, id: Date.now() }];
case DELETE_TODO:
return state.filter((toDo) => toDo.id !== action.id);
default:
return state;
}
};
변화는 순수 함수로 작성되어야 한다. 즉, 상태 트리의 변화에 대한 내용은 프로그래머가 순수한 함수로 작성해야 합니다.
리듀서는 이전 상태와 액션을 parameter로 받아 다음의 상태를 반환하는 함수입니다.
이때, 리듀서는 이전 상태를 변경하는 대신 Action에 따라 새로운 상태 객체를 생성해서 반환합니다.
작은 프로젝트에선 하나의 리듀서로 상태를 관리하는데에 충분하지만 앱이 커져감에 따라 특정 상태 트리를 조작하는 더 작은 단위의 리듀서로 나누고 다시 combine하는 것도 가능합니다.
// reducer를 나눈 예시
function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)