WEB/REACT

9. Immutable & Ducks & 예제

AKI 2019. 7. 11. 01:21

Immutable : https://immutable-js.github.io/immutable-js/

 

Immutable.js

Immutable collections for JavaScript Immutable data cannot be changed once created, leading to much simpler application development, no defensive copying, and enabling advanced memoization and change detection techniques with simple logic. Persistent data

immutable-js.github.io

- 자바 스크립트에서 불변성 데이터를 다룰 수 있도록 도와주는 라이브러리

 


Ducks : 액션타입, 액션생성함수, 리듀서를 모두 한 파일에서 모듈화하여 관리하기 위한 파일 구조

 

https://github.com/erikras/ducks-modular-redux

 

erikras/ducks-modular-redux

A proposal for bundling reducers, action types and actions when using Redux - erikras/ducks-modular-redux

github.com

https://redux-actions.js.org/

 

Read Me

 

redux-actions.js.org

- Ducks 구조 규칙

1) export default 를 이용하여 리듀서를 내보내야 한다.

2) export를 이용하여 액션 생성 함수를 내보내야 한다.

3) 액션 타입 이름은 npm-module-or-app/reducer/ACTION_TYPE 형식으로 만들어야 한다.

4) 외부 리듀서에서 모듈의 액션 타입이 필요할 때는 액션 타입을 내보내도 된다.

 

 


 

이번에는 저번에 만든 일정관리에 리덕스를 적용해보겠습니다.

 

create-react-app reduxexample2
...
yarn add redux react-redux redux-actions immutable
yarn add node-sass
yarn eject
// y선택

config/webpack.config.js

{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
},
'sass-loader'
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},

부분을 아래와같이 수정

{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders(
{
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
}).concat({
loader: require.resolve('sass-loader'),
options: {
includePaths: [paths.appSrc + '/styles'],
sourceMap: isEnvProduction && shouldUseSourceMap
}
}
),
// Don't consider CSS imports dead code even if the
// containing package claims to have no side effects.
// Remove this when webpack adds a warning or an error for this.
// See https://github.com/webpack/webpack/issues/6571
sideEffects: true,
},

 

yarn add open-color

 

 

src/modules/input.js

import { Map } from 'immutable';
import { handleActions, createAction } 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);

 

src/modules/todos.js

/* 구현할 액션
INSERT : 추가
TOGGLE : 토글
REMOVE : 삭제
*/

import { Map, List} from 'immutable';
import { handleActions, createAction} 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) => {
      /* payload 안에 있는 id, text, done에 대한 레퍼런스를 만들어줍니다.
      레퍼런스를 만들지 않고, 바로 push(Map(action.payload))를 해도 되지만,
      나중에 이 코드를 보게 됐을 때, 
      이 액션이 어떤 데이터를 처리하는지 쉽게 보기 위해서 하는 작업입니다. */
      const { id, text, done } = action.payload;
  
      return state.push(Map({
        id,
        text,
        done
      }));
    },
    [TOGGLE]: (state, action) => {
      const { payload: id } = action;
      // = const id = action.payload;
      /* 비구조화 할당을 통하여 id라는 레퍼런스에 action.payload란 값을 넣습니다.
      이 작업이 필수는 아니지만, 나중에 이 코드를 보게 되었을 때 여기서의 payload가
      어떤 값을 의미하는지 이해하기 쉬워집니다. */
  
      // 전달받은 id 를 가지고 index 를 조회합니다.
      const index = state.findIndex(todo => todo.get('id') === id);
  
      // updateIn을 통해 현재 값을 참조하여 반대값으로 설정합니다.
      return state.updateIn([index, 'done'], done => !done);
      /* updateIn을 사용하지 않는다면 다음과 같이 작성할 수도 있습니다.
      return state.setIn([index, 'done'], !state.getIn([0, index]));
      어떤 코드가 더 편해 보이나요? 둘 중에 여러분이 맘에 드는 코드로 작성하면 됩니다.
      */
    },
    [REMOVE]: (state, action) => {
      const { payload: id } = action;
      const index = state.findIndex(todo => todo.get('id') === id);
      return state.delete(index);
    }
  }, initialState);

 

두 모듈을 합치는 src/modules/index.js

// 리듀서 합치기
import input from './input';
import todos from './todos';
import { combineReducers } from 'redux';

export default combineReducers({
    input,
    todos
});

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 {
  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}
      />
    );
  }
}

/* 이번에는 mapStateToProps와 mapDispatchToProps 함수에 대한 레퍼런스를
따로 만들지 않고, 그 내부에 바로 정의해주었습니다.*/
export default connect(
  (state) => ({
    value: state.input.get('value')
  }),
  (dispatch) => ({
    /* bindActionCreators를 사용하면 다음 작업들을 자동으로 해줍니다:
      {
          actionCreator: (...params) => dispatch(actionCreator(...params))
      }
      그래서 이전에 우리가 했었던 것처럼 하나하나 dispatch할 필요가 없습니다.
      예를 들면 InputActions의 경우 다음과 같은 작업이 되어 있는 것이죠.
      InputActions: {
        setInput: (value) => dispatch(inputActions.setInput(value))
      }
      나중에 이를 호출할 때는 this.props.InputActions.setInput을 호출하면 됩니다.
    */
    InputActions: bindActionCreators(inputActions, dispatch),
    TodosActions: bindActionCreators(todosActions, dispatch)
  })
)(TodoInputContainer);

 

src/containers/TodoListContainers.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 {
  handleToggle = (id) => {
    const { TodosActions } = 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}
      />
    );
  }
}

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

src/components/TodoItem/TodoItem.js

import React, { Component } from 'react';
import styles from './TodoItem.scss';
import classNames from 'classnames/bind';

const cx = classNames.bind(styles);

class TodoItem extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.done !== nextProps.done;
  }

  render() {
    const {done, children, onToggle, onRemove} = this.props;
    /* 위 코드에선 비구조화 할당을 통하여 this.props 안에 있는
       done, children, onToggle, onRemove 에 대한 레퍼런스를 만들어주었습니다. */
    return (
      <div className={cx('todo-item')} onClick={onToggle}>
        <input className={cx('tick')} type="checkbox" checked={done} readOnly/>
        <div className={cx('text', { done })}>{children}</div>
        <div className={cx('delete')} onClick={(e) => {
          onRemove();
          e.stopPropagation();
          }
        }>[지우기]</div>
      </div>
    );
  }
}

export default TodoItem;

 

src/components/TodoList/TodoList.js

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

class TodoList extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return this.props.todos !== nextProps.todos;
  }  

// todo가 List이므로 get방식으로 변경
  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>
    );
  }

}

export default TodoList;

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

 

결과)

shouldComponentUpdate를 이용하여 Virtual DOM 불필요한 리렌더링을 방지하고

리덕스로 인해 복잡한 상황에서도 액션, 디스패치, 구독으로 인해 상태 관리가 잘되는것을 볼수있습니다.

 

적용전

 

적용후

 

 

반응형