Redux : 자바스크립트 앱을 위한 예측 가능한 상태 저장소
Single Source of Truth 단 하나의 진실의 원천
redux 는 하나의 상태를 갖는다. 이때의 상태는 객체를 말한다. 하나의 객체에 애플리케이션에 필요한 모든 상태를 넣는 방식이다. 이로써 앱의 복잡도를 낮출 수 있다.
redux 의 상태는 외부로부터 격리되어 있다. 이때 상태로의 접근 및 수정은 dispatcher
혹은 reducer
를 통해서만 가능하다. 데이터를 가져가는 것도 getState
를 통해서만 가능하다.
이렇게 외부로부터 데이터를 직접적으로 제어할 수 없도록 하여 데이터가 예기치 않게 수정되는 것을 막아 앱을 보다 예측 가능하게 만들 수 있다.
만약 상태가 변경되면 해당 상태를 사용하는 앱에 변경사실을 알린다.
이런 과정을 통해 UNDO 와 REDO 를 쉽게 할 수 있다.
hot module reloading
lunit.gitbook.io/redux/basics/actions
액션은 앱에서 스토어로 보내는 데이터 묶음이다. 이들이 스토어의 유일한 정보원이 되며 store.dispatch() 를 통해 액션을 보낼 수 있다.
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
액션은 자바스크립트 객체이며 어떤 형태의 액션이 실행될지에 대한 type
속성을 반드시 가져야 한다. 보통 타입은 문자열 상수로 정의한다. 혹은 타입들을 아래와 같이 별도의 모듈 단위로 관리할 수도 있다.
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
액션 생산자는 액션을 만드는 함수이며 액션과는 다르다.
실제로 액션을 보내기 위해서는 결과값을 dispatch()
함수로 넘겨야 한다.
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
dispatch(addTodo(text))
혹은 아래와 같이 자동으로 액션을 보내주는 바인드된 액션 생산자를 만들수도 있다.
const boundAddTodo = (text) => dispatch(addTodo(text));
boundAddTodo(text);
// actions.js
/*
* 액션 타입
*/
export const ADD_TODO = 'ADD_TODO'
export const COMPLETE_TODO = 'COMPLETE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
/*
* 다른 상수
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
/*
* 액션 생산자
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
export function completeTodo(index) {
return { type: COMPLETE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
액션은 무언가 동작이 발생한다는 것에 대해 기술하나 그 결과적으로 앱의 상태가 어떻게 바뀌는지는 특정하지 않는다. 이런 일을 reducer 가 하게 된다.
리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 순수함수다.
(previousState, action) => newState
이 형태의 함수를 Array.prototype.reduce(reducer, ?initialValue) 로 넘길 것이라서 리듀서라고 부른다. 이때 리듀서는 순수하게 유지하는 것이 매우 중요하므로 리듀서 내에서 인수 변경 / API 호출, 라우팅 전환같은 사이드 이펙트 / Date.now() 같은 순수하지 않은 함수 호출 등을 하면 절대 안된다.
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state = initialState, action) {
// 지금은 아무 액션도 다루지 않고
// 주어진 상태를 그대로 반환합니다.
return state
}
이때 visibilityFilter 에 따라 다른 todo 를 보이고 싶기 때문에 코드를 다음과 같이 switch case 문으로 수정한다.
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
default:
return state
}
}
주의점
- 위의 예시코드는 배열을 상태로 갖는 케이스였기에 원본 state 를 변경하는 것과 같은 사이드 이펙트를 피하기 위해 원본은 그대로 두고 Object.assign() 을 통해 복사본을 만들어 반환하거나 아니면 ES7 의 object spread syntax 의
{ ...state, ...newState }
를 사용하는 방식을 사용해야 한다.Object.asign() 이 ES6 일부이나 대부분의 브라우저에서 구현되지 않았으니 폴리필을 사용하거나 Babel 플러그인이나 _.assign() 같은 다른 라이브러리의 헬퍼를 사용해야 한다고 한다.- can i use 사이트를 찾아보니 이젠 왠만한 브라우저에서는 지원이 되고 있는것 같으니 그냥 사용해도 될 것 같다.
보일러 플레이트 코드
- 최소한의 변경으로 여러 곳에서 재사용되며 반복적으로 비슷한 형태를 띄는 코드
- 어원 : 1890년대에 광고나 칼럼과 같이 계속 사용되는 텍스트 인쇄판은 강철로 찍기 시작했고 이를 Boilerplate 라고 불렀다.
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
case ADD_TODO:
return Object.assign({}, state, {
todos: [...state.todos, {
text: action.text,
completed: false
}]
});
case COMPLETE_TODO:
return Object.assign({}, state, {
todos: [
...state.todos.slice(0, action.index),
Object.assign({}, state.todos[action.index], {
completed: true
}),
...state.todos.slice(action.index + 1)
]
});
default:
return state
}
}
위의 코드에서 todos 와 visibilityFilter 가 서로 독립적이니 서로 분리해줄 수 있다.
import { combineReducers } from 'redux';
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions';
const { SHOW_ALL } = VisibilityFilters;
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.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
});
export default todoApp;
여러 리듀서를 하나로 합쳐준다.
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
});
// 위와 동일
function reducer(state, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
};
}
action
: 무슨 일이 일어날지에 대해
reducer
: 액션에 따라 상태를 수정
store
: action, reducer 를 함께 가져오는 객체
store 가 하는일
redux 애플리케이션에서 하나의 스토어만 가질 수 있으니 로직을 나누고 싶다면 하나의 스토어에 여러 리듀서를 결합하여 사용할 수 있다.
import { createStore } from 'redux';
import todoApp from './reducers';
let store = createStore(todoApp);
// let store = createStore(todoApp, window.STATE_FROM_SERVER);
스토어엔 createStore 를 통해 첫 번째 인자로 리듀서를 전달하고 두번째 인자로 초기 상태를 지정해줄 수도 있다.
import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from './actions';
// 초기 상태를 기록한다.
console.log(store.getState());
// 상태가 바뀔때마다 기록한다.
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
// 액션들을 보낸다.
store.dispatch(addTodo('Learn about actions'));
store.dispatch(addTodo('Learn about reducers'));
store.dispatch(addTodo('Learn about store'));
store.dispatch(completeTodo(0));
store.dispatch(completeTodo(1));
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED));
// 상태 변경을 더 이상 받아보지 않는다.
unsubscribe();
store.dispatch(action)
호출 { type: 'LIKE_ARTICLE', articleId: 42 };
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Megan' } };
{ type: 'ADD_TODO', text: 'Read the Redux docs.'};
// 애플리케이션의 현재 상태(할일 목록과 선택된 필터)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [{
text: 'Read the docs.',
complete: false
}]
};
// 실행되는 액션(할일 추가)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
};
// 리듀서가 다음 상태를 반환함
let nextState = todoApp(previousState, action);
리듀서는 다음 상태를 계산하는 순수함수이니 몇번을 실행하거나 언제 실행하든 상관없이 항상 같은 출력이 나와야 한다.
function todos(state = [], action) {
// Somehow calculate it...
return nextState;
}
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// Somehow calculate it...
return nextState;
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
});
store.subscribe(listener)
를 통해 등록된 모든 리스터가 불러내지고 이들은 현재 상태를 얻기 위해 store.getState()
를 호출한다.