엑스트라..

축하합니다! 우리가 구현해야 할 주요 작업들은 모두 끝났습니다. 하지만, 이 튜토리얼은 완전히 끝나지는 않았습니다. 앞으로 해결해야 할 항목이 두가지 남았습니다. 근데 이것들은 꼭 해야 하는 것은 아닙니다. 다만, 개발을 어쩌면 조금 더 편하게 해줄 수 는 있습니다.

조회 할 때 .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 를 일일히 하는것이 귀찮아진다고 느낄때면, 이러한 방법이 있다는 것을 참고하세요~

results matching ""

    No results matching ""