이 튜토리얼은 5개의 포스트로 나뉘어진 이어지는 강좌입니다. 목차를 확인하시려면 여기를 참고하세요.
4장. Ducks 구조와 redux-actions 사용하기
Redux 공식 문서에서는, ActionType, Action, Reducer 이 3가지를 따로 따로 다룹니다. 그러다보니, 하나의 액션을 추가하려면 3개의 다른 파일들을 수정해야하죠. 가끔씩은, 액션생성자를 하나하나 만들고 또 그것들을 dispatch 하는 과정이 귀찮게 느껴질때도 있습니다. 상태관리를 편하게 하자고 리덕스를 사용하는건데 오히려 더 복잡해지는것 같기도 합니다.
4장에서는 리덕스를 최대한 편하게 사용하기 위한 몇가지 팁을 다뤄보도록 하겠습니다.
섹션 4-1와 4-2에선 우선 필요한 개념을 알아보고, 4-3에서 카운터에 적용을 해보도록 하죠.
4-1. Ducks 구조
Ducks 구조에는 Reducer 파일 안에 액션타입과 액션생성자 함수를 함께 넣어서 관리하고 이를 ‘모듈’ 이라고 부릅니다.
한번 Ducks 구조를 사용한 예시 모듈을 볼까요?
// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
최상단엔 액션타입을 정의하고,
리듀서를 정의하여 export default
로 내보내고,
같은 파일에서 액션생성자도 export
를 통해 내보냅니다.
이 패턴에서 따를 규칙은, 액션 타입을 만들때 npm-module-or-app/reducer/ACTION_TYPE
의 형식으로 만들어야 합니다.
만약에 NPM 모듈을 만드는게 아니라면, reducer/ACTION_TYPE
형식으로만 만들어도 상관없습니다. 이렇게 접두사를 달아주는 이유는 서로다른 리듀서에서 액션이름이 중첩되는것을 방지하기 위함 입니다.
그리고, 리듀서를 만들때는 export default
로 내보내고, 액션생성자는 export
로 내보내주어야합니다.
준수해야 할 규칙은 이게 전부입니다.
곧 우리가 만들었던 카운터를, Ducks 패턴으로 작성해볼텐데요. 그 전에, 액션을 더욱 쉽게 관리 할 수 있 해주는 redux-actions
에 대해서 알아보겠습니다.
4-2. redux-actions 를 통한 더 쉬운 액션관리
redux-actions
패키지에는 리덕스의 액션들을 관리하기 위한 유용한 createAction
과 handleActions
가 있습니다. 이번 섹션에서는 이 함수들이 어떤 기능을하는지 알아보도록 하겠습니다.
createAction 을 통한 액션생성 자동화
리덕스에서, 액션을 만들다보면 드는 의문이, 이걸 굳이 하나하나 액션 생성자를 만들어야하나? 입니다.
예를 들어, 우리가 기존에 만들었던 increment
와 decrement
코드를 다시 한번 읽어봅시다.
export const increment = (index) => ({
type: types.INCREMENT,
index
});
export const decrement = (index) => ({
type: types.DECREMENT,
index
});
그냥 파라미터로 전달받은 값을 객체에 넣어주는것 뿐인데 이걸 자동화할수도 있지 않을까요?
createAction
을 사용한다면 위 작업을 다음과 같이 자동화 시켜 줄 수 있습니다.
export const increment = createAction(types.INCREMENT);
export const decrement = createAction(types.DECREMENT);
하지만, 이런식으로 하면 그 파라미터의 값이 index
가 될 지 뭐가 될 지 모릅니다. 그렇기 때문에, 파라미터로 전달받은 값을 액션의 payload
값으로 설정해줍니다. 따라서 increment(3)
가 실행된다면, 다음과 같이 객체를 만들어주겠죠.
{
type: 'INCREMENT',
payload: 5
}
setColor의 경우엔 어떨까요?
export const setColor = createAction(types.SET_COLOR);
setColor({index: 5, color: '#fff'})
/* 결과:
{
type: 'SET_COLOR',
payload: {
index: 5,
color: '#fff'
}
}
*/
어때요? 이해가 가나요? 액션이 갖고있을 수 있는 변수를 payload
로 통일하므로서, 액션을 생성하는것을 자동화 할 수 있게 되는 것이지요. 편리하지만, 단점으로는 코드를 봤을때 해당 액션생성자가 파라미터로 필요한 값이 뭔지 모르기때문에, 그 위에 주석을 작성해주어야 합니다.
switch 문 대신 handleActions 사용하기
리듀서에서 액션의 type 에 따라 다른 작업을 하기 위해서 우리는 switch
문을 사용했지요. 하지만 이 방식엔 아주 중요한 결점이 한가지 있습니다. 바로, scope
가 리듀서 함수로 설정되어있다는것이지요.
그렇기 때문에 서로 다른 case
에서 let
이나 const
를 통하여 변수를 선언하려고 하다보면 같은 이름이 중첩될시엔 에러가 발생합니다.
그렇다고해서 각 case
마다 함수를 정의하는건 코드를 읽기 힘들어질것이구요..
이 문제를 해결해주는것이 바로 handleActions
입니다. 이 함수를 사용하면 다음과 같은 방식으로 해결 할 수 있습니다.
const reducer = handleActions({
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
}, { counter: 0 });
첫번째 파라미터로는 액션에 따라 실행 할 함수들을 가지고있는 객체, 두번째 파라미터로는 상태의 기본 값 (initialState) 를 넣어주면 됩니다.
한번, 직접 사용해보고싶지 않나요? 이제, 방금 배운 Duck 구조와 redux-actions 을 저희 카운터에 적용을 해보겠습니다.
4-3. 적용하기
자, 이제 우리가 배웠던것들을 우리의 프로젝트에 적용을 해봅시다.
모듈 작성
우선, redux-actions
를 설치하세요.
yarn add redux-actions
그 다음, src
디렉토리에 modules
디렉토리를 만들고, 그 안에 index.js
파일을 생성하세요.
src/modules/index.js
먼저, redux-actions 에서 createAction 과 handleActions 를 불러오고 액션타입을 선언하도록 하겠습니다.
추후 immutable 의 Map 과 List 도 필요해질테니 미리 불러오겠습니다.
import { createAction, handleActions } from 'redux-actions';
import { Map, List } from 'immutable';
// 액션 타입
const CREATE = 'counter/CREATE';
const REMOVE = 'counter/REMOVE';
const INCREMENT = 'counter/INCREMENT';
const DECREMENT = 'counter/DECREMENT';
const SET_COLOR = 'counter/SET_COLOR';
이제 방금 만든 액션타입들을 가지고 createAction 을 통하여 액션생성자들을 만들겠습니다. 이 액션생성자들은 전부 밖으로 내보내주어야 하니 export
키워드를 사용하세요.
// 액션 생성자
export const create = createAction(CREATE); // color
export const remove = createAction(REMOVE);
export const increment = createAction(INCREMENT); // index
export const decrement = createAction(DECREMENT); // index
export const setColor = createAction(SET_COLOR); // { index, color }
해당 액션생성자들이, 어떤 파라미터를 받아야하는지, 주석에 메모로 달아둡니다.
다음, 초기상태를 리듀서 파일에서 복사해오세요.
// 초기 상태를 정의합니다
const initialState = Map({
counters: List([
Map({
color: 'black',
number: 0
})
])
});
그리고, handleActions 를 통하여 리듀서의 틀을 만들어주겠습니다.
export default handleActions({
[CREATE]: (state, action) => state,
[REMOVE]: (state, action) => state,
[INCREMENT]: (state, action) => state,
[DECREMENT]: (state, action) => state,
[SET_COLOR]: (state, action) => state,
}, initialState);
우리의 액션타입에는 접두사가 들어가있기 때문에 그냥 CREATE:
를 하면 안되고, [CREATE]:
로 해주어야합니다.
그 다음에는, 기존에 작성했던 리듀서를 참조하여 하나하나 구현해보세요. SET_COLOR
을 제외한 액션들에서는 파라미터들의 이름이 payload
로 통합되었습니다.
SET_COLOR
의 경우엔 index 와 color 값이 payload 객체 안에 들어가있기에 action.payload.index
이런식으로 작성을 해야하구요
그리고 이전 리듀서에서는 switch
를 사용했기에 counters
라는 공유되는 변수가 있었지만, 이젠 각 함수마다 counters
를 준비해주세요.
export default handleActions({
[CREATE]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.push(
Map({
color: action.payload,
number: 0
})
))
},
[REMOVE]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.pop())
},
[INCREMENT]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload,
(counter) => counter.set('number', counter.get('number') + 1))
);
},
[DECREMENT]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload,
(counter) => counter.set('number', counter.get('number') - 1))
);
},
[SET_COLOR]: (state, action) => {
const counters = state.get('counters');
return state.set('counters', counters.update(
action.payload.index,
(counter) => counter.set('color', action.payload.color))
);
},
}, initialState);
변화 반영
이제 actions, reducers 디렉토리는 쓸모없어졌으니 삭제하세요.
그리고, src 디렉토리의 index.js
에서, ./reducers
파일을 불러오는 대신에 ./modules
를 불러오세요
src/index.js
import reducers from './modules';
다음, App.js
과 CounterListContainer.js
컴포넌트에서도 ./actions
대신에 ./modules
를불러오세요.
src/containers/App.js
src/containers/CounterListContainer.js
import * as actions from '../modules';
자, 이제 카운터가 정상적으로 작동하는지 확인하세요.