액션
데이터를 변경하려면 어떤 행동을 해야하는지에 대한 정의가 필요하다.
새 할 일의 추가를 나타내는 액션의 예시이다. 액션은 평범한 자바스크립트 객체다.
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
- type - 어떤 형태의 액션이 실행될지 나타내는 속성. 앱이 충분히 커지게 되면 타입들을 별도의 파일로 분리
- type 외에 액션 객체의 구조는 마음대로 짜면 된다. ( 물론 권장사항이 있다. - https://github.com/acdlite/flux-standard-action )
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
액션 생산자
액션을 만드는 함수다.
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
실제로 액션을 보내려면 결과값을 dispatch() 함수에 넘긴다. ( 자세한건 뒷 부분에서 확인해보자 )
dispatch(addTodo(text))
dispatch(completeTodo(index))
리듀서
앞에서 살펴본 액션은 무언가 일어난다를 기술한 것이지, 상태가 어떻게 바뀌는지는 정의하지 않았다. 이 일을 리듀서가 한다.
상태 설계하기
Redux에서 애플리케이션의 모든 상태는 하나의 객체에 저장된다.
먼저, 할일 앱을 예시로 살펴보자.
- 현재 선택된 필터 ( 다 보여줌, 완료만 보여줌 등 )
- 할 일의 실제 목록
{
visibilityFilter: 'SHOW_ALL',
todos: [{
text: 'Consider using Redux',
completed: true,
}, {
text: 'Keep all state in a single tree',
completed: false
}]
}
더 복잡한 앱에서는 각기 다른 개체들이 서로를 참조하게 만들어야 한다. 이를 위해서는 데이터베이스에서 배웠던 정규화 할 것을 권장한다. 모든 개체가 ID를 가지고, ID를 통해 다른 개체나 목록을 참조하도록 해야한다. 즉, 앱의 상태를 데이터베임스라고 생각하면 된다. https://github.com/paularmstrong/normalizr 를 참고하면 된다. 예를 들어 상태 안에 todosById: { id -> todo } 와 todos : array<id> 처럼 구현하는 것이 실제 앱에서는 더 적절하다. 단지 이것은 보여주기 용이다.
액션 다루기
리듀서는 이전 상태와 액션을 받아서 다음 상태를 반환하는 순수 함수다. 즉, 이전의 상태(목록들)와 어떤 행동인지(할일더하기)를 넘겨주면 그걸 바탕으로 새로운 상태를 만들어준다.
(previousState, action) => newState
리듀선는 순수 함수이기 때문에 순수하게 유지하는 것(?)은 매우 중요하다. 리듀서에서 절대로 하지 말아야 할 것은
- 인수들을 변경(mutate)하기
- API 호출 / 라우팅 전환같은 사이드이펙트를 일으키기
- Date.now()나 Math.random() 같이 순수하지 않은 함수를 호출하기.
사이드 이펙트가 어떻게 일어나느지는 심화과정에서 확인해보자. 인수가 주어지면, 계산만 가능하다.
먼저 초기 상태를 정의하자
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
// 지금은 아무 액션도 다루지 않고
// 주어진 상태를 그대로 반환합니다.
return state
}
이제 SET_VISIBILITY_FILTER 를 처리해보자. 우리가 해야 할일은 visibilityFilter 를 바꾸는 것 뿐이다.,
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()을 통해 복사본을 만들었다.
- default 케이스에 대해 이전의 state 를 반환하였다. 알수 없는 액션에 대해서는 이전의 state 를 반환하는 것이 좋다.
더 많은 액션 다루기
우리의 리듀서가 할일을 더하는 ADD_TODO를 다룰 수 있도록 확장해보자.
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
}]
});
default:
return state;
}
}
앞에서와 마찬가지로 state나 그 필드들을 직접 쓰는 대신 새 객체를 반환하였다.
만약 이런 코드를 자주 작성해야 한다면 React update 같은 헬퍼를 사용하는 것이 좋다.
switch (action.type)
{
case types.AUTH_LOGIN:
return update(state,{
login :{
status : { $set :'WAITING'}
}
});
...
리듀서 쪼개기
리듀서 내에서 너무 많은 액션을 분류하다 보니 코드가 번접스러워 졌다.
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 가 독립적으로 수정된다. 즉 두가지 함수로 분리해보자.
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;
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
};
}
- todoApp 은 관리할 상태의 조각만 넘기고, todos 는 그 조각을 어떻게 수정할지 알고 있다.
- 이것을 리듀서 조합이라 부르고, Redux 앱을 만드는 기본 패턴이다.
각각의 리듀서는 전체 상태(state)에서 자기가 수정해야 할 부분만을 관리한다. 모든 리듀서의 state 매개변수는 서로 다르고, 자신이 관리하는 부분에 해당한다. 앱이 더 커지면, 리듀서를 별도의 파일로 분리해서 관리하는 것도 좋은 방법이다.
마지막으로 Redux는 combineReducers() 라는 유틸리티를 제공한다. 이를 이용하면 다음과 같이 재작성이 가능하다.
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
});
export default todoApp;
이는 아래와 같은 코드다.
export default function todoApp(state, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
};
}
ES6을 이해하는 사람들을 위한 한마디
combineReducers 는 객체를 기대하기 때문에, 모든 최상위 리듀서들을 각기 다른 파일에 놓고 export 한 다음 import * as reducers 를 이용해
각각의 이름을 키로 가지는 객체를 얻을 수 있다.
import { combineReducers } from 'redux'; import * as reducers from './reducers'; const todoApp = combineReducers(reducers);
import { combineReducers } from 'redux'; import
memo from './memo';
import
authentication from './authentication';
const todoApp = combineReducers({ authentication, memo } );
Source Code - reducers.js
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;
'React' 카테고리의 다른 글
Redux 시작하기 기초 - (마지막) React와 함께 사용하기 (0) | 2017.11.21 |
---|---|
Redux 기초 - (2) 스토어(Store), 데이터 흐름 (0) | 2017.11.21 |
#4. React 성능과 렌더링 (0) | 2017.06.19 |
Tip (0) | 2017.05.23 |
#3. Redux (0) | 2017.05.23 |