투두리스트 구현하기

자 이제 우측에 보여지는 투두리스트를 구현해보겠습니다. 투두리스트는 카운터와 달리 필요한 액션의 수가 좀 더 많고, 상태의 구조도 아주 조금 더 복잡합니다.

우선, 필요한 액션들은 다음과 같습니다:

  1. CHANGE_INPUT: 인풋 수정
  2. INSERT: 새 항목 추가
  3. TOGGLE: 삭제선 켜고 끄기
  4. REMOVE: 제거하기

그리고 상태의 구조는 다음과 같습니다:

{
  input: '',
  todos: [
    {
      id: 0,
      text: '걷기',
      checked: false
    },
    {
      id: 1,
      text: '코딩하기',
      checked: true
    }
  ]
}

우리는 상태를 업데이트 할 때, 기존의 객체는 건들이지 않고 새 객체를 만들어주어야 합니다. 즉, 불변성을 유지해가면서 상태를 업데이트해야한다는 것이죠. 다음 예제를 한번 읽어보세요.

let nextState = null;
// input 값을 바꾼 새 객체를 만들기
nextState = {
  ...state,
  input: '새로운 값'
};
// todos 에 항목 추가하기
nextState = {
  ...state,
  todos: state.todos.concat({
    id: 2,
    text: '새로운거',
    checked: false
  })
};
// 0번째 항목 checked 값 반전하기
const nextTodos = [...state.todos];
nextTodos[0] = {
  ...nextTodos[0],
  checked: !nextTodos.checked
};
nextState = {
  ...state,
  todos: nextTodos
}

일반 자바스크립트를 사용하여 불변성을 유지해가면서 상태를 업데이트하는것은 그렇게 어려운 작업은 아니지만, 귀찮은 것은 사실이며, 조금 더 많은 코드를 작성해야하는 것 또한 사실입니다. Immutable.js 를 사용하면 위 코드들은 다음과 같이 간단하게 바뀔 수 있습니다.

let nextState = null;
// input 값을 바꾼 새 객체를 만들기
nextState = state.set('input', '새로운 값');
// todos 에 항목 추가하기
nextState = state.update('todos', todos => todos.push(Map({ id:2, text: '새로운거', checked: false })));
// 0번째 항목 checked 값 반전하기
nextState = state.updateIn(['todos', 0, 'checked'], checked => !checked);

Immutable.js 를 꼭 써야하는 것은 아닙니다만, 익숙해지면 개발이 매우 편해집니다. 만약에 Immutable.js 가 익숙하지 않다면 관련 포스트 를 꼭 한번 읽고 오세요!

todo.js 모듈 작성하기

그럼, 투두리스트를 위한 todo.js 모듈을 작성해봅시다! 먼저 액션 생성함수들을 작성해볼까요?

src/store/modules/todo.js

import { createAction } from 'redux-actions';

const CHANGE_INPUT = 'todo/CHANGE_INPUT';
const INSERT = 'todo/INSERT';
const TOGGLE = 'todo/TOGGLE';
const REMOVE = 'todo/REMOVE';

export const changeInput = createAction(CHANGE_INPUT);
export const insert = createAction(INSERT);
export const toggle = createAction(TOGGLE);
export const remove = createAction(REMOVE);

우리가 이번에 만든 액션함수들은, 참조해야 할 값들이 필요합니다. 예를들어서, changeInput 은 다음 어떤 값으로 바뀌어야 할지를 알려주는 값이 필요하고, insert 는 추가 할 내용, 그리고, toggle 과 remove 는 어떤 id 를 수정해야 할 지 알려주어야겠죠.

createAction 을 통하여 만든 액션생성함수에 파라미터를 넣어서 호출하면, 자동으로 payload 라는 이름으로 통일되어 설정됩니다.

다음과 같이 말이죠:

changeInput('새로운 값');
// { type: 'todo/CHANGE_INPUT', payload: '새로운 값' }

가끔씩은 여러종류의 값을 전달해야 될 때도 있겠죠. 그럴 땐 이렇게 객체를 넣어주면 됩니다.

const multi = createAction('MULTI');
multi({ foo: 1, bar: 2 });
// { type: 'MULTI', payload: { foo: 1, bar: 2 } }

그런데, 코드상에서 해당 액션함수들이 어떠한 파라미터를 받는지 명시하고 싶을 수도 있습니다. createAction 함수는 세가지의 파라미터를 받는데요, 첫번째는 액션이름, 두번째는 payloadCreator, 세번째는 metaCreator 입니다. 두번째와 세번째 파라미터는 payload 값과 meta 값을 지정해주는 함수인데요, 다음 코드를 보면 이해하기 쉽습니다.

예제:

const sample = createAction('SAMPLE', (value) => value + 1, (value) => value - 1);
sample(1);
// { type: 'SAMPLE', payload: 2, meta: 0 }

payloadCreator 가 생략되어있을때는, 액션생성함수의 파라미터가 그대로 payload 값으로 설정되며, metaCreator 가 생략되어있을때에는, meta 값을 따로 생성하지 않습니다.

따라서, 우리가 작성한 코드는 다음과 같이 수정 할 수 있습니다.

src/store/modules/todo.js

import { createAction } from 'redux-actions';

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);

이렇게 하면, 위 액션 생성함수들이 어떠한 값을 파라미터로 받는지 알겠지요?

자, 그럼 이어서 초기상태를 정의하고, 리듀서 함수도 작성해보겠습니다.

src/store/modules/todo.js

import { createAction, handleActions } from 'redux-actions';
import { Map, 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);
// 순수한 리듀서를 위하여, id 값의 증가는 리듀서가 아닌 액션 생성 함수에서! (또는 다른 곳에서)
export const insert = createAction(INSERT, text => ({
  text,
  id: id++
}));
export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

let id = 0; // todo 아이템에 들어갈 고유 값 입니다

const initialState = Map({
  input: '',
  todos: List()
});

export default handleActions({
  // 한줄짜리 코드로 반환 할 수 있는 경우엔 다음과 같이 블록 { } 를 생략 할 수 있습니다.
  [CHANGE_INPUT]: (state, action) => state.set('input', action.payload),
  [INSERT]: (state, { payload: { id, text } }) => {
    const item = Map({
      id,
      text,
      checked: false
    });
    return state.update('todos', todos => todos.push(item));
  },
  [TOGGLE]: (state, { payload: id }) => {
    // id 값을 가진 index 를 찾아서 checked 값을 반전시킵니다
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.updateIn(['todos', index, 'checked'], checked => !checked);
  },
  [REMOVE]: (state, { payload: id }) => {
    // id 값을 가진 index 를 찾아서 지웁니다.
    const index = state.get('todos').findIndex(item => item.get('id') === id);
    return state.deleteIn(['todos', index]);
  }
}, initialState);

Immutable.js 를 쓰지 않았다면, 아마 코드의 양은 1.5배 정도 많아졌을 것입니다. 더군다나, handleAction 을 쓰지 않았다면 위와 같이 action 비구조화 할당을 하거나, 한줄로 처리 할 방법도 없었겠죠.

새로운 모듈을 다 만들었다면, combineReducers 안에 넣어주어야합니다.

src/store/modules/todo.js

import { combineReducers } from 'redux';
import counter from './counter';
import todo from './todo';

export default combineReducers({
  counter,
  todo
});

TodoContainer 컴포넌트 작성하기

우리가 CounterContainer 에 했던것과 동일한 작업을 진행해주겠습니다. TodosContainer 에서 Todos 를 불러와서 렌더링 하고, 기존에 App 에서 Todos 가 들어가던 자리를 TodosContainer 로 대체해주는 것이죠.

src/containers/TodosContainer.js

import React, { Component } from 'react';
import Todos from 'components/Todos';

class TodosContainer extends Component {
  render() {
    return (
      <Todos />
    );
  }
}

export default TodosContainer;

src/components/App.js

import React, { Component } from 'react';
import CounterContainer from 'containers/CounterContainer';
import TodosContainer from 'containers/TodosContainer';
import AppTemplate from './AppTemplate';


class App extends Component {
  render() {
    return (
      <AppTemplate
        counter={<CounterContainer />}
        todos={<TodosContainer />}
      />
    );
  }
}

export default App;

본격적으로 시작하기 전에, Todos 컴포넌트를 한번 살펴볼까요?

src/components/Todos.js

import React from 'react';
import { List, Map } from 'immutable';

const TodoItem = ({ id, text, checked, onToggle, onRemove }) => (
  <li 
    style={{
      textDecoration: checked ? 'line-through' : 'none'
    }} 
    onClick={() => onToggle(id)}
    onDoubleClick={() => onRemove(id)}>
    {text}
  </li>
)

const Todos = ({todos, input, onInsert, onToggle, onRemove, onChange }) => {

  const todoItems = todos.map(
    todo => {
      const { id, checked, text } = todo.toJS();
      return (
        <TodoItem
          id={id}
          checked={checked}
          text={text}
          onToggle={onToggle}
          onRemove={onRemove}
          key={id}
        />
      )
    }
  )
  return (
    <div>
      <h2>오늘 할 일</h2>
      <input value={input} onChange={onChange}/>
      <button onClick={onInsert}>추가</button>
      <ul>
        { todoItems }
      </ul>
    </div>
  );
};

Todos.defaultProps = {
  todos: List([
    Map({
      id: 0,
      text: '걷기',
      checked: false
    }),
    Map({
      id: 1,
      text: '코딩하기',
      checked: true
    })
  ]),
  input: ''
};

export default Todos;

그냥 전형적인 투두리스트입니다. 할일목록이 들어있는 todos 값과, 인풋 내용 input 값을 받아옵니다. 그리고 4가지 함수도 props 로 받아오죠.

  • onInsert: 추가 (버튼 클릭 시)
  • onToggle: 삭제선 켜고 끄기 (아이템 클릭 시)
  • onRemove: 제거 (아이템 더블 클릭 시)
  • onChange: 인풋 값 수정

props 로 받아온 todos 는 Immutable List 형태입니다. Immutable List 는 완전한 배열은 아니지만, 리액트에서 호환이 되기 때문에 map 함수를 사용하여 컴포넌트 List 를 렌더링 했을 때 오류 없이 렌더링 할 수 있습니다. 추가적으로, List 안에 들어있는 것들은 Map 이므로, 내부 아이템들을 조회 할 때에는 .get() 을 사용하거나, .toJS() 를 통하여 일반 객체로 변환 후 사용해주어야 합니다.

자, 그러면 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 {
  handleChange = (e) => {
    // 인풋 값 변경
    const { TodoActions } = this.props;
    TodoActions.changeInput(e.target.value);
  }

  handleInsert = () => {
    // 아이템 추가
    const { input, TodoActions } = this.props;
    TodoActions.insert(input); // 추가하고
    TodoActions.changeInput(''); // 기존 인풋값 비우기
  }

  handleToggle = (id) => {
    // 삭제선 켜고 끄기
    const { TodoActions } = this.props;
    TodoActions.toggle(id);
  }

  handleRemove = (id) => {
    // 아이템 제거
    const { TodoActions } = this.props;
    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(
  // state 를 비구조화 할당 해주었습니다
  ({ todo }) => ({
    // immutable 을 사용하니, 값을 조회 할 때엔느 .get 을 사용해주어야하죠.
    input: todo.get('input'),
    todos: todo.get('todos')
  }),
  (dispatch) => ({
    TodoActions: bindActionCreators(todoActions, dispatch)
  })
)(TodosContainer);

이제 투두리스트에서 인풋을 수정해보고, 버튼을 클릭해서 새 투두아이템을 생성해보세요. 그리고 생성된 아이템을 클릭하여 삭제선을 껐다 켜보시고, 더블클릭하여 제거해보세요.

results matching ""

    No results matching ""