7. 대기자 명단 만들기

이제 카운터 아래쪽에 있는 대기자 명단 기능의 상태관리 작업을 해줄 차례입니다. 이번에는, redux-actions 라는 라이브러리를 활용하여 리덕스 모듈 작성을 더욱 손쉽게 하는 방법을 알아보겠습니다.

액션 타입 정의하기

우선, waiting.js 라는 리덕스 모듈을 만들고, 필요한 액션 타입들을 정의해주겠습니다.

src/store/modules/waiting.js

const CHANGE_INPUT = 'waiting/CHANGE_INPUT'; // 인풋 값 변경
const CREATE = 'waiting/CREATE'; // 명단에 이름 추가
const ENTER = 'waiting/ENTER'; // 입장
const LEAVE = 'waiting/LEAVE'; // 나감

각 액션들마다, 필요로 하는 파라미터값들이 다릅니다. 예를들어서 CHANGE_INPUT 과 CREATE 는 문자열 상태의 값을 받아와야 할 것이고, ENTER 와 LEAVE 는 아이템의 id 값을 받아와야 하겠죠.

액션 생성함수 만들기

우리는 잠시 후에, 액션 생성 함수를 간편하게 만들 수 있게 해주는 redux-actions 의 createAction 이라는 함수를 사용하여 작성해볼건데요, 이는 FSA 규칙을 따르는 액션 객체를 만들어주는데, 이 FSA 규칙은 읽기 쉽고, 유용하고, 간단한 액션 객체를 만들기 위해서 만들어졌습니다.

FSA 에선 다음 조건들을 필수적으로 갖추고있어야 합니다.

  • 순수 자바스크립트 객체이며,
  • type 값이 있어야 합니다.

그리고 다음 사항들은 선택적으로 필요합니다.

  • error 값이 있음
  • payload 값이 있음
  • meta 값이 있음

여기서 payload 부분을 주시하셔야 되는데, FSA 규칙을 따르는 액션 객체는, 액션에서 사용 할 파라미터의 필드명을 payload 로 통일 시킵니다. 이를 통하여, 우리는 액션 생성 함수를 훨씬 더 쉽게 작성 할 수 있습니다.

error 는 에러가 발생 할 시 넣어 줄 수 있는 값이고, meta 는 상태 변화에 있어서 완전히 핵심적이지는 않지만 참조할만한 값을 넣어줍니다.

그럼, 먼저 FSA 규칙을 준수하여 액션 생성 함수를 작성해볼까요?

src/store/modules/waiting.js

const CHANGE_INPUT = 'waiting/CHANGE_INPUT'; // 인풋 값 변경
const CREATE = 'waiting/CREATE'; // 명단에 이름 추가
const ENTER = 'waiting/ENTER'; // 입장
const LEAVE = 'waiting/LEAVE'; // 나감

// **** FSA 규칙을 따르는 액션 생성 함수 정의
export const changeInput = text => ({ type: CHANGE_INPUT, payload: text });
export const create = text => ({ type: CREATE, payload: text });
export const enter = id => ({ type: ENTER, payload: id });
export const leave = id => ({ type: LEAVE, payload: id });

createAction 사용하기

위 코드는, createAction 을 사용하게 된다면 다음과 같이 대체 할 수 있습니다.

src/store/modules/waiting.js

import { createAction } from 'redux-actions';

const CHANGE_INPUT = 'waiting/CHANGE_INPUT'; // 인풋 값 변경
const CREATE = 'waiting/CREATE'; // 명단에 이름 추가
const ENTER = 'waiting/ENTER'; // 입장
const LEAVE = 'waiting/LEAVE'; // 나감

// **** createAction 으로 액션 만들기
export const changeInput = createAction(CHANGE_INPUT, text => text);
export const create = createAction(CREATE, text => text);
export const enter = createAction(ENTER, id => id);
export const leave = createAction(LEAVE, id => id);

훨씬 가독성이 좋죠? createAction 함수에서 두번째 파라미터로 받는 부분은 payloadCreator 로서, payload 를 어떻게 정할 지 설정합니다. 만약에 생략하면 기본적으로 payload => payload 형태로 되기 때문에, 위 코드를 다음과 같이 작성해도 작동에 있어선 차이가 없습니다.

export const leave = createAction(LEAVE);
leave(1); // { type: LEAVE, payload: 1 }

그 대신에 이렇게 두번째 파라미터를 생략한다면, 해당 액션에서 어떠한 값을 payload 로 설정하게 했더라? 하고 헷갈릴 가능성이 있습니다.

현재 상황에서는, 데이터를 새로 생성 할 때마다 고유 id 값을 주어야 하는데요, 이전에 우리는 "변화를 일으키는 함수, 리듀서는 순수한 함수여야 합니다." 라고 배웠습니다. 데이터에 고유 id 를 주는 작업은 리듀서에서 발생하면 안되고, 액션이 스토어에 디스패치 되기 전에 이뤄져야 합니다.

그걸 하기 위해서, 액션 생성함수를 조금 수정해주는 방법도 있습니다. 다음과 같이 코드를 수정해주세요.

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

const CHANGE_INPUT = 'waiting/CHANGE_INPUT'; // 인풋 값 변경
const CREATE = 'waiting/CREATE'; // 명단에 이름 추가
const ENTER = 'waiting/ENTER'; // 입장
const LEAVE = 'waiting/LEAVE'; // 나감

let id = 3;
// createAction 으로 액션 생성함수 정의
export const changeInput = createAction(CHANGE_INPUT, text => text);
export const create = createAction(CREATE, text => ({ text, id: id++ }));
export const enter = createAction(ENTER, id => id);
export const leave = createAction(LEAVE, id => id);

export default handleActions({});

그러면, 이렇게 작동하게 됩니다.

create('hello');
{ type: CREATE,  payload: { id: 3, text: 'hello' } }
create('bye');
{ type: CREATE,  payload: { id: 4, text: 'bye' } }

초기 상태 및 리듀서 정의

이제 이 모듈의 초기 상태와 리듀서를 정의해주겠습니다. 리듀서를 만들 땐, redux-actions 의 handleActions 를 사용하면 훨씬 편하게 작성 할 수 있습니다.

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

const CHANGE_INPUT = 'waiting/CHANGE_INPUT'; // 인풋 값 변경
const CREATE = 'waiting/CREATE'; // 명단에 이름 추가
const ENTER = 'waiting/ENTER'; // 입장
const LEAVE = 'waiting/LEAVE'; // 나감

let id = 3;
// createAction 으로 액션 생성함수 정의
export const changeInput = createAction(CHANGE_INPUT, text => text);
export const create = createAction(CREATE, text => ({ text, id: id++ }));
export const enter = createAction(ENTER, id => id);
export const leave = createAction(LEAVE, id => id);

// **** 초기 상태 정의
const initialState = {
  input: '',
  list: [
    {
      id: 0,
      name: '홍길동',
      entered: true,
    },
    {
      id: 1,
      name: '콩쥐',
      entered: false,
    },
    {
      id: 2,
      name: '팥쥐',
      entered: false,
    },
  ],
};

// **** handleActions 로 리듀서 함수 작성
export default handleActions(
  {
    [CHANGE_INPUT]: (state, action) => ({
      ...state,
      input: action.payload,
    }),
    [CREATE]: (state, action) => ({
      ...state,
      list: state.list.concat({
        id: action.payload.id,
        name: action.payload.text,
        entered: false,
      }),
    }),
    [ENTER]: (state, action) => ({
      ...state,
      list: state.list.map(
        item =>
          item.id === action.payload
            ? { ...item, entered: !item.entered }
            : item
      ),
    }),
    [LEAVE]: (state, action) => ({
      ...state,
      list: state.list.filter(item => item.id !== action.payload),
    }),
  },
  initialState
);

handleActions 를 사용하면, 더이상 switch / case 문을 사용 할 필요가 없이 각 액션 타입마다 업데이터 함수를 구현하는 방식으로 할 수 있어서 가독성이 더 좋아집니다.

여기서, CREATE, ENTER, LEAVE 의 액션의 경우엔 배열을 다뤄야 하는 것들이라, concat, map, filter 를 사용하여 불변성을 유지하면서 배열에 새로운 값을 지정해주었습니다.

루트 리듀서에 포함 시키기

새 리듀서를 만들었으니, 루트 리듀서쪽에도 포함시켜줘야겠죠?

src/store/modules/index.js

import { combineReducers } from 'redux';
import counter from './counter';
import waiting from './waiting'; // **** 불러오기

export default combineReducers({
  counter,
  waiting, // **** 추가
});

WaitingListContainer 만들기

이제 컨테이너 컴포넌트를 만들어주겠습니다!

src/containers/WaitingListContainer.js

import React, { Component } from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import * as waitingActions from '../store/modules/waiting';
import WaitingList from '../components/WaitingList';

class WaitingListContainer extends Component {
  // 인풋 변경 이벤트
  handleChange = e => {
    const { WaitingActions } = this.props;
    WaitingActions.changeInput(e.target.value);
  };
  // 등록 이벤트
  handleSubmit = e => {
    e.preventDefault();
    const { WaitingActions, input } = this.props;
    WaitingActions.create(input); // 등록
    WaitingActions.changeInput(''); // 인풋 값 초기화
  };
  // 입장
  handleEnter = id => {
    const { WaitingActions } = this.props;
    WaitingActions.enter(id);
  };
  // 나가기
  handleLeave = id => {
    const { WaitingActions } = this.props;
    WaitingActions.leave(id);
  };
  render() {
    const { input, list } = this.props;
    return (
      <WaitingList
        input={input}
        waitingList={list}
        onChange={this.handleChange}
        onSubmit={this.handleSubmit}
        onEnter={this.handleEnter}
        onLeave={this.handleLeave}
      />
    );
  }
}

const mapStateToProps = ({ waiting }) => ({
  input: waiting.input,
  list: waiting.list,
});

// 이런 구조로 하면 나중에 다양한 리덕스 모듈을 적용해야 하는 상황에서 유용합니다.
const mapDispatchToProps = dispatch => ({
  WaitingActions: bindActionCreators(waitingActions, dispatch),
  // AnotherActions: bindActionCreators(anotherActions, dispatch)
});
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(WaitingListContainer);

그리고, App.js 에서 WaitingList 를 WaitingListContainer 로 교체하세요.

src/App.js

import React, { Component } from 'react';

import './App.css';
import PaletteContainer from './containers/PaletteContainer';
import CounterContainer from './containers/CounterContainer';
import WaitingListContainer from './containers/WaitingListContainer'; // **** 불러오기

class App extends Component {
  render() {
    return (
      <div className="App">
        <PaletteContainer />
        <CounterContainer />
        <WaitingListContainer /> {/* **** 교체하기 */}
      </div>
    );
  }
}

export default App;

WaitingList 내부 구현

껍데기만 구현되어있었던 WaitingList 컴포넌트에 전달한 props 들을 유의미하게 사용하도록 기능들을 구현해주겠습니다.

src/components/WaitingList.js

import React from 'react';
import './WaitingList.css';

const WaitingItem = ({ text, entered, onEnter, onLeave }) => {
  return (
    <li>
      <div className={`text ${entered ? 'entered' : ''}`}>{text}</div>
      <div className="buttons">
        <button onClick={onEnter}>입장</button>
        <button onClick={onLeave}>나감</button>
      </div>
    </li>
  );
};

const WaitingList = ({
  input, // **** 추가됨
  waitingList,
  onChange, // **** 추가됨
  onSubmit, // **** 추가됨
  onEnter,
  onLeave,
}) => {
  // **** 데이터를 컴포넌트 리스트로 변환
  const waitingItems = waitingList.map(w => (
    <WaitingItem
      key={w.id}
      text={w.name}
      entered={w.entered}
      id={w.id}
      onEnter={() => onEnter(w.id)}
      onLeave={() => onLeave(w.id)}
    />
  ));
  return (
    <div className="WaitingList">
      <h2>대기자 명단</h2>
      {/* form 과 input 에 이벤트 및 값 설정 */}
      <form onSubmit={onSubmit}>
        <input value={input} onChange={onChange} />
        <button>등록</button>
      </form>
      <ul>{waitingItems}</ul> {/* 하드코딩된것을 컴포넌트 배열로 교체 */}
    </div>
  );
};

export default WaitingList;

이제 대기자 명단 구현도 끝났습니다!

Edit colorful-counter [waiting-list]

results matching ""

    No results matching ""