엑스트라..
축하합니다! 우리가 구현해야 할 주요 작업들은 모두 끝났습니다. 하지만, 이 튜토리얼은 완전히 끝나지는 않았습니다. 앞으로 해결해야 할 항목이 두가지 남았습니다. 근데 이것들은 꼭 해야 하는 것은 아닙니다. 다만, 개발을 어쩌면 조금 더 편하게 해줄 수 는 있습니다.
조회 할 때 .get 하는 것이 맘에 안든다! 그렇다면 Record 사용하기
Immutable.js 를 사용해서 상태를 업데이트하는것은 정말로 편합니다. 하지만, 값을 조회 할 때 마다 .get 을 사용해야 한다는 것은 조금 귀찮을 수도 있는데요, 만약 Map 대신 Record 를 사용하게 된다면 이 부분이 해결됩니다. Record 를 사용하면, Map 을 다룰때와 똑같이 사용 할 수 있는데 차이점은, state.input, state.todos 이런식으로 직접 조회 할수 있게 됩니다.
todo 모듈을 다음과 같이 수정해주세요.
src/store/modules/todo.js
import { createAction, handleActions } from 'redux-actions';
import { Record, List } from 'immutable';
const CHANGE_INPUT = 'todo/CHANGE_INPUT';
const INSERT = 'todo/INSERT';
const TOGGLE = 'todo/TOGGLE';
const REMOVE = 'todo/REMOVE';
export const changeInput = createAction(CHANGE_INPUT, value => value);
export const insert = createAction(INSERT, text => text);
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);
let id = 0; // todo 아이템에 들어갈 고유 값 입니다
// Record 함수는 Record 형태 데이터를 만드는 함수를 반환합니다.
// 따라서, 만든 다음에 뒤에 () 를 붙여줘야 데이터가 생성됩니다.
const initialState = Record({
input: '',
todos: List()
})();
// Todo 아이템의 형식을 정합니다.
const TodoRecord = Record({
id: id++,
text: '',
checked: false
})
export default handleActions({
[CHANGE_INPUT]: (state, action) => state.set('input', action.payload),
[INSERT]: (state, { payload: text }) => {
// TodoRecord 를 사용해야 아이템도 Record 형식으로 조회 가능합니다.
// 빠져있는 값은, 기본값을 사용하게 됩니다 (checked: false)
const item = TodoRecord({ id: id++, text });
return state.update('todos', todos => todos.push(item));
},
[TOGGLE]: (state, { payload: id }) => {
const index = state.get('todos').findIndex(item => item.get('id') === id);
return state.updateIn(['todos', index, 'checked'], checked => !checked);
},
[REMOVE]: (state, { payload: id }) => {
const index = state.get('todos').findIndex(item => item.get('id') === id);
return state.deleteIn(['todos', index]);
}
}, initialState);
그러면, 이에 따라 TodosContainer 를 다음과 같이 수정해도 되겠죠?
src/containers/TodosContainer.js
import React, { Component } from 'react';
import Todos from 'components/Todos';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as todoActions from 'store/modules/todo';
class TodosContainer extends Component {
(...)
}
export default connect(
({ todo }) => ({
// 일반 객체 다루듯이 다루면 됩니다.
input: todo.input,
todos: todo.todos
}),
(dispatch) => ({
TodoActions: bindActionCreators(todoActions, dispatch)
})
)(TodosContainer);
추가적으로, 내부에 있는 아이템들도 Record 형태이기 때문에, Todos 컴포넌트에서 .toJS() 도 생략해줘도 됩니다.
src/components/Todos.js
- 컴포넌트로 map 하는 부분
const todoItems = todos.map(
todo => {
const { id, checked, text } = todo;
return (
<TodoItem
id={id}
checked={checked}
text={text}
onToggle={onToggle}
onRemove={onRemove}
key={id}
/>
)
}
)
Record 를 쓰면, .get, .getIn 이런걸 쓰지 않아도 되기 때문에 편리한점이 많습니다. 하지만 그 대신에 제한도 조금 생깁니다. 예를들어서, 다음과 같은 코드는 제대로 작동하지 않습니다.
const HumanRecord = Record({
name: 'John',
age: 10
});
let human = HumanRecord();
human = human.set('job', 'developer');
// Error: Cannot set unknown key "job" on n
Record 를 사용하면, 초반에 Record 에 정의한 값만 설정 할 수 있습니다. 때문에, 데이터가 지니고 있는 key 가 유동적이라면, 필요한 부분에 Map 을 사용하는 것이 옳은 선택입니다.
액션생성함수를 미리 bind 하기
리덕스를 사용하면서 의문점이 들었습니다. 지금의 경우엔, CounterContainer 에서 counter 모듈의 액션생성함수를 참조하고, TodosContainer 에서 todo 모듈의 액션생성함수를 참조하고 있는데요, 실제 프로젝트에서는 한 종류의 모듈을 여러곳에서 사용 할 일이 많습니다. 예를들어서, form 이라는 모듈에서 폼 만을 관리하는 모듈을 만들 수도 있는 것이고, modal 이라는 모듈을 만들어서 모든 모달들을 관리 할 수도 있고.. 또 header 라는 모듈을 만들어서 헤더에 관련된 액션들을 관리 하게 될 수도 있죠.
그런데, 그러한 액션들을 사용 할 때마다 mapDispatchToProps 에 해당하는 부분을 계속 작성하는 것이 저는 굉장히 귀찮다고 생각했습니다. 그래서, 최근 액션생성함수를 미리 bind 하는 것을 시도해봤는데, 꽤 만족스러웠어서 여기에도 소개 해볼까 합니다.
일단, 이것을 하기 위해선, 리덕스 스토어 인스턴스가 모듈화되어 불러 올 수 있는 상태여야 합니다. (우리는 이미 그렇게 했죠) 참고로 리덕스 매뉴얼에서 FAQ 란을 보면 스토어 인스턴스를 모듈화하여 내보내는것을 권장하고 있지 않다고 적혀있는데 그 이유는 나중에 리덕스 앱을 분리시킬때 힘들것이기 때문이라고 적어놓았는데요, 여러개의 컴포넌트에서 스토어를 직접 불러와서 접근하는 것이 아니기 때문에 이 부분은 문제되지 않습니다.
자! 그러면 액션생성함수를 미리 bind 해봅시다!
src/store/actionCreators.js
import { bindActionCreators } from 'redux';
import * as counterActions from './modules/counter';
import * as todoActions from './modules/todo';
import store from './index';
const { dispatch } = store;
export const CounterActions = bindActionCreators(counterActions, dispatch);
export const TodoActions = bindActionCreators(todoActions, dispatch);
그러면, CounterContainer 는 다음과 같이 수정하여 사용 할 수 있습니다.
src/containers/CounterContainer.js
import React, { Component } from 'react';
import Counter from 'components/Counter';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { CounterActions } from 'store/actionCreators';
class CounterContainer extends Component {
handleIncrement = () => {
CounterActions.increment();
}
handleDecrement = () => {
CounterActions.decrement();
}
render() {
const { handleIncrement, handleDecrement } = this;
const { number } = this.props;
return (
<Counter
onIncrement={handleIncrement}
onDecrement={handleDecrement}
number={number}
/>
);
}
}
/* 첫번째 파라미터 mapStateToProps: props 값으로 넣어 줄 상태를 정의해줍니다.
컴포넌트를 리덕스와 연동 할 떄에는 connect 를 사용합니다.
connect() 의 결과는, 컴포넌트에 props 를 넣어주는 함수를 반환합니다.
반환된 함수에 우리가 만든 컴포넌트를 넣어주면 됩니다. */
export default connect(
(state) => ({
number: state.counter.number
})
)(CounterContainer);
이렇게 mapDispatchToProps 는 생략 할 수 있게 되죠.
TodosContainer 도 마찬가지 입니다.
src/containers/TodosContainer.js
import React, { Component } from 'react';
import Todos from 'components/Todos';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { TodoActions } from 'store/actionCreators';
class TodosContainer extends Component {
handleChange = (e) => {
TodoActions.changeInput(e.target.value);
}
handleInsert = () => {
const { input } = this.props;
TodoActions.insert(input);
TodoActions.changeInput('');
}
handleToggle = (id) => {
TodoActions.toggle(id);
}
handleRemove = (id) => {
TodoActions.remove(id);
}
render() {
const { handleChange, handleInsert, handleToggle, handleRemove } = this;
const { input, todos } = this.props;
return (
<Todos
input={input}
todos={todos}
onChange={handleChange}
onInsert={handleInsert}
onToggle={handleToggle}
onRemove={handleRemove}
/>
);
}
}
export default connect(
({ todo }) => ({
input: todo.input,
todos: todo.todos
})
)(TodosContainer);
아직 이 방법은 실험적입니다. 무조건 이렇게 하라고 권장하지는 않겠습니다 :) 하지만, 여러분들이 만약에 앞으로 작업을 하면서 mapDispatchToProps 를 일일히 하는것이 귀찮아진다고 느낄때면, 이러한 방법이 있다는 것을 참고하세요~