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;
이제 대기자 명단 구현도 끝났습니다!