All Articles

[Redux 3탄] 애플리케이션에 리덕스 적용 예제


Redux 3탄

실제 프로젝트에 적용할 시, 흐름을 정리하겠습니다.

1) 리덕스 관련 모듈 설치하기

2) 모듈 만들기

3) 스토어 만들기

4) 컨테이너 컴포넌트 만들기

5) 프로젝트 마무리


1. 리덕스 관련 모듈 설치하기

$ yarn add redux react-redux redux-actions immutable


2. 모듈 만들기

modules 폴더를 만들어 줍니다. modules 디렉터리에는 Ducks 구조를 적용한 리덕스 관련 코드를 넣을 것입니다.


[ Ducks 구조로 만드는 리덕스 모듈 생성 흐름 ]

액션 타입 정의하기 —> 액션 생성 함수 만들기 —> 초기 상태 정의하기 —> 리듀서 정의하기


- 인풋 상태를 관리하는 input 모듈

src/modules/input.js

import { Map } from 'immutable';
import { createAction, handleActions } from 'redux-actions';

// 액션 타입 정의하기
const SET_INPUT = 'input/SET_INPUT';

// 액션 생성 함수 만들기
export const setInput = createAction(SET_INPUT);

// 초기 상태 정의하기
const initialState = Map({
  value: ''
});

// 리듀서 정의하기
export default handleActions({
  [SET_INPUT]: (state, action) => {
    return state.set('value', action.payload)
  }
}, initialState);

- 일정들의 상태를 관리하는 todos 모듈

src/modules/todos.js

import { Map, List } from 'immutable';
import { createAction, handleActions } from 'redux-actions';

// 액션 타입 정의하기
const INSERT = 'todos/INSERT';
const TOGGLE = 'todos/TOGGLE';
const REMOVE = 'todos/REMOVE';

// 액션 생성 함수 만들기
export const insert = createAction(INSERT);
export const toggle = createAction(TOGGLE);
export const remove = createAction(REMOVE);

// 초기 상태 정의하기
const initialState = List([
  Map({
    id: 0,
    text: '리액트 공부하기',
    done: true
  }),
  Map({
    id: 1,
    text: '컴포넌트 스타일링 해보기',
    done: false
  })
]);

// 리듀서 정의하기
export default handleActions({
  [INSERT]: (state, action) => {
    const { id, text, done } = action.payload;

    return state.push(Map({
      id,
      text,
      done
    }));
  },
  [TOGGLE]: (state, action) => {
    const { payload: index } = action;

    return state.updateIn([index, 'done'], done => !done);
  },
  [REMOVE]: (state, action) => {
    const { payload: index } = aciton;
    return state.delete(index);
  }
}, initialState)


- 모듈 인덱스 파일 생성

combineReducers를 사용하여 리듀서를 합쳐서 내보내는 파일을 생성합니다.

src/modules/index.js

import input from './input';
import todos from './todos';
import { combineReducers } from 'redux';

export default combineReducers({
  input,
  todos
});


3. 스토어 생성하기

리덕스 스토어를 생성하고, Provider로 리액트 프로젝트에 적용해봅시다.

src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './styles/main.scss';
import App from './components/App';

import modules from './modules';
import { createStore } from 'redux';
import { Provider } from 'react-redux';

const store = createStore(modules, window.devToolsExtension && window.devToolsExtension());

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>
  , document.getElementById('root')
);


4. 컨테이너 컴포넌트 생성

- TodoInputContainer 생성

각 메서드를 구현하기 전에 먼저 store의 상태와 액션 생성 함수들을 연결시켜 보겠습니다.

src/containers/TodoInputContainer.js

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

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

// 액션 생성 함수들을 한꺼번에 불러옵니다.
import * as inputActions from '../modules/input';
import * as todosActions from '../modules/todos';

class TodoInputContainer extends Component {
  render() {
    return (
      <TodoInput />
    );
  }
}

/* mapStateToProps, mapDispatchToProps 함수에 대한
레퍼런스 대신, 내부에 바로 정의
*/
export default connect(
  (state) => ({
    value: state.input.get('value')
  }),
  (dispatch) => ({
    /*
    InputActions: {
      setInput: (value) => dispatch(inputActions.setInput(value))
    }
    */
    InputActions: bindActionCreators(inputActions, dispatch),
    todosActions: bindActionCreators(todosActions, dispatch)
  })
)(TodoInputContainer);

계속 이어서 같은 파일에 추가

(...)
class TodoInputContainer extends Component {
  id = 1
  getId = () => {
    return ++this.id;
  }

  handleChange = (e) => {
    const { value } = e.target;
    const { InputActions } = this.props;
    InputActions.setInput(value);
  }

  handleInsert = () => {
    const { InputActions, TodosActions, value } = this.props;
    const todo = {
      id: this.getId(),
      text: value,
      done: false
    };
    TodosActions.insert(todo);
    InputActions.setInput('');
  }

  render() {
    const { value } = this.props;
    const { handleChange, handleInsert } = this;
    return (
      <TodoInput
        onChange={handleChange}
        onInsert={handleInsert}
        value={value}
      />
    );
  }
}


- TodoListContainer 생성

src/containers/TodoListContainer.js

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

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import * as todosActions from '../modules/todos';

class TodoListContainer extends Component {
  render() {
    return(
      <TodoList/>
    );
  }
}

export default connect(
  (state) => ({
    todos: state.todos
  }),
  (dispatch) => ({
    TodosActions: bindActionCreators(todosActions, dispatch)
  })
)(TodoListContainer)

계속 같은 파일에 필요한 메서드를 구현합니다.

class TodoListContainer extends Component {
  handleToggle = (id) => {
    const { TodoActions } = this.props;
    TodosActions.toggle(id);
  }
  handleRemove = (id) => {
    const { TodosActions } = this.props;
    TodosActions.remove(id);
  }
  render() {
    const { todos } = this.props;
    const { handleToggle, handleRemove } = this;

    return (
      <TodoList
        todos={todos}
        onToggle={handleToggle}
        onRemove={handleRemove}
      />
    );
  }
}


5. 프로젝트 마무리

src/components/TodoList/TodoList.js

(...)
render() {
  const { todos, onToggle, onRemove } = this.props;
  const todoList = todos.map(
    todo => (
      <TodoItem
        key={todo.get('id')}
        done={todo.get('done')}
        onToggle={() => onToggle(todo.get('id'))}
        onRemove={() => onRemove(todo.get('id'))}>
        {todo.get('text')}
      </TodoItem>
    )
  );

  return (
    <div>
      {todoList}
    </div>
  );
}

src/components/App.js

import React, { Component } from 'react';
import PageTemplate from './PageTemplate';

import TodoInputContainer from '../containers/TodoInputContainer';
import TodoListContainer from '../containers/TodoListContainer';

class App extends Component {
  render(){
    return (
      <PageTemplate>
        <TodoInputContainer />
        <TodoListContainer />
      </PageTemplate>
    )
  }
}

export default App;