import { createStore } from 'redux';
import rootReducer from './module/rootReducer';
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);
이전 포스팅에서는 store 를 생성할 때는 redux 가 제공하는 createStore 를 이용해 생성했다.
그런데 configureStore 를 사용하면 아래와 같이 코드를 줄일 수 있다.
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({ reducer: rootReducer });
Redux-Toolkit 의 configureStore 는 Redux 의 createStore 를 활용한 API 로써,
위 처럼 reducer 필드를 필수적으로 넣어주어야 하며 default 로 redux devtool 을 제공한다.
비교 코드는 다음과 같다.
import { createStore } from 'redux';
import rootReducer from './module/rootReducer';
import { configureStore } from '@reduxjs/toolkit';
// before
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);
// after
const store = configureStore({ reducer: rootReducer });
createAction 은 action 을 보다 간결하게 만들어 줄 수 있게 해준다.
이전 포스팅의 action 선언부는 다음과 같다.
// Action Type
const MODE_REMOVE = 'REMOVE';
const MODE_SAVE = 'SAVE';
const MODE_SELECT_ROW = 'SELECT_ROW';
// Action Create Function
export const boardSave = (saveData) => ({
type: MODE_SAVE,
saveData: {
boardId: saveData.boardId,
boardTitle: saveData.boardTitle,
boardContent: saveData.boardContent
}
});
export const boardRemove = (boardId) => ({
type: MODE_REMOVE,
boardId: boardId
});
export const boardSelectRow = (boardId) => ({
type: MODE_SELECT_ROW,
boardId: boardId
});
위 코드를 createAction 을 적용하면 아래와 같다.
// Action Type
const MODE_REMOVE = 'REMOVE';
const MODE_SAVE = 'SAVE';
const MODE_SELECT_ROW = 'SELECT_ROW';
// Action Create function
export const boardSave = createAction(MODE_SAVE, saveData => saveData);
export const boardRemove = createAction(MODE_REMOVE, boardId => boardId);
export const boardSelectRow = createAction(MODE_SELECT_ROW, boardId => boardId);
createAction 은 type 만 넣어주어도 자기가 알아서 type 을 가진 action object 를 생성해준다.
만약 이 생성함수를 호출할 때 parameter 를 추가로 넣어준다면 이는 그대로 payload 필드에 자동으로 들어가게 된다.
아래의 예를 보자.
const MODE_INCREMENT = 'INCREMENT';
const increment = createAction(MODE_INCREMENT);
let action = increment(); // return { type: 'INCREMENT' }
let action = increment(5); // return { type: 'INCREMENT', payload: 5 }
일반적으로 기존에 reducer 를 사용할 때 switch 등의 조건문으로 action 의 type 을 구분해 특정 로징을 수행했다.
뿐만 아니라 default 를 항상 명시해 주었는데 이러한 귀찮은 것들을 createReducer 를 사용하면 해결할 수 있다.
이 또한 이전 포스팅의 reducer 부분을 예로 들어 살펴보자.
export default function boardReducer(state=initialState, action) {
switch(action.type) {
case MODE_REMOVE:
return { ...state, boards: state.boards.filter(row => row.boardId !== action.boardId) };
case MODE_SAVE:
if(action.saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...action.saveData, boardId: state.lastId+1 }), selectRowData: {} };
} else {
return { ...state, boards: state.boards.map(data => data.boardId === action.saveData.boardId ? {...action.saveData}: data), selectRowData: {} };
}
case MODE_SELECT_ROW:
return { ...state, selectRowData: state.boards.find(row => row.boardId === action.boardId) };
default:
return state;
}
};
이를 위에서 언급한대로 createReducer 를 사용해
switch 와 default 를 없애고 아래와 같이 보다 가독성이 좋게 만들었다.
export default createReducer(initialState, {
[MODE_REMOVE]: (state, { payload: boardId }) => {
return { ...state, boards: state.boards.filter(row => row.boardId !== boardId) }
},
[MODE_SAVE]: (state, { payload: saveData}) => {
if(saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...saveData, boardId: state.lastId+1 }), selectRowData: {} }
} else {
return { ...state, boards: state.boards.map(data => data.boardId === saveData.boardId ? {...saveData} : data), selectRowData: {} }
}
},
[MODE_SELECT_ROW]: (state, { payload: boardId }) => {
return { ...state, selectRowData: state.boards.find(row => row.boardId === boardId) }
}
})
위 코드를 보면 알 수 있듯이 switch 문이 없어졌고,
createReducer 의 첫번 째 인자값인 initialState 가 default 값이기에 default 문 또한 필요없어졌다.
그리고 [MODE_REMOVE], [MODE_SAVE], [MODE_SELECT_ROW]... 처럼 액션 타입을 집어넣었는데,
이전에 다룬 createAction 에서 만든 액션 생성 함수를 그대로 집어 넣어도 된다.
아래처럼 말이다.
// Action Type
const MODE_REMOVE = 'REMOVE';
const MODE_SAVE = 'SAVE';
const MODE_SELECT_ROW = 'SELECT_ROW';
// Action Create function
export const boardSave = createAction(MODE_SAVE, saveData => saveData);
export const boardRemove = createAction(MODE_REMOVE, boardId => boardId);
export const boardSelectRow = createAction(MODE_SELECT_ROW, boardId => boardId);
.
.
.
.
.
export default createReducer(initialState, {
[boardRemove]: (state, { payload: boardId }) => {
return { ...state, boards: state.boards.filter(row => row.boardId !== boardId) }
},
[boardSave]: (state, { payload: saveData}) => {
if(saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...saveData, boardId: state.lastId+1 }), selectRowData: {} }
} else {
return { ...state, boards: state.boards.map(data => data.boardId === saveData.boardId ? {...saveData} : data), selectRowData: {} }
}
},
[boardSelectRow]: (state, { payload: boardId }) => {
return { ...state, selectRowData: state.boards.find(row => row.boardId === boardId) }
}
})
이렇게 바로 액션 생성 함수를 집어넣어 사용할 수 있는 이유는
createAction 함수가 toString() 메소드를오버라이드 했기 때문이다.
만약 boardRemove 같은 경우 createAction 이 "REMOVE" 형태로 return 해주는 셈이다.
방금 위에서 다룬 리듀서는 Directory 구조를 action, reducer 로 나누지 않고 하나로 합쳐 Ducks 패턴으로 작성했다.
createSlice 또한 Ducks 패턴을 사용해 action 과 reducer 전부를 가지고 있는 함수이다.
createSlice 의 기본 형태는 다음과 같다.
createSlice({
name: 'reducerName',
initialState: [],
reducers: {
action1(state, payload) {
//action1 logic
},
action2(state, payload) {
//action2 logic
},
action3(state, payload) {
//action3 logic
}
}
})
name 속성은 액션의 경로를 잡아줄 해당 이름을 나타내고, initialState 는 초기 state 를 나타낸다.
reducer 는 우리가 이전에 사용하던 action 의 구분을 주어 해당 action 의 로직을 수행하는 방법과 동일하다.
차이점이라면 기존에는 Action Create Function 과 Action Type 을 선언해 사용했었다면,
createSlice 의 reducers 에서는 이 과정을 건너뛰고 Action 을 선언하고 해당 Action 이 dispatch 되면
바로 state 를 가지고 해당 action 을 처리한다.
즉, reducers 안의 코드들은 Action Type, Action Create Function, Reducer 의 기능이 합쳐져 있는 셈이다.
먼저 Redux-Toolkit 을 적용시키지 않은 이전 포스팅의 boardReducer 부분을 다시 보자.
// Action Type
const MODE_REMOVE = 'REMOVE';
const MODE_SAVE = 'SAVE';
const MODE_SELECT_ROW = 'SELECT_ROW';
// Action Create Function
export const boardSave = (saveData) => ({
type: MODE_SAVE,
saveData: {
boardId: saveData.boardId,
boardTitle: saveData.boardTitle,
boardContent: saveData.boardContent
}
});
export const boardRemove = (boardId) => ({
type: MODE_REMOVE,
boardId: boardId
});
export const boardSelectRow = (boardId) => ({
type: MODE_SELECT_ROW,
boardId: boardId
})
// initState
const initialState = {
boards: [
{
boardId: 1,
boardTitle: '제목1',
boardContent: '내용내용내용1'
},
{
boardId: 2,
boardTitle: '제목2',
boardContent: '내용내용내용2'
},
{
boardId: 3,
boardTitle: '제목3',
boardContent: '내용내용내용3'
},
{
boardId: 4,
boardTitle: '제목4',
boardContent: '내용내용내용4'
},
{
boardId: 5,
boardTitle: '제목5',
boardContent: '내용내용내용5'
}
],
lastId: 5,
selectRowData: {}
}
// Reducer
export default function boardReducer(state=initialState, action) {
switch(action.type) {
case MODE_REMOVE:
return { ...state, boards: state.boards.filter(row => row.boardId !== action.boardId) };
case MODE_SAVE:
if(action.saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...action.saveData, boardId: state.lastId+1 }), selectRowData: {} };
} else {
return { ...state, boards: state.boards.map(data => data.boardId === action.saveData.boardId ? {...action.saveData}: data), selectRowData: {} };
}
case MODE_SELECT_ROW:
return { ...state, selectRowData: state.boards.find(row => row.boardId === action.boardId)
};
default:
return state;
}
};
자, 이제 일일히 선언해 놓은 Action Type, Action Create Function, InitialState, Reducer 에
redux-toolkit 의 createSlice 를 이용해 수정해 보겠다.
다음과 같다.
// createSlice 사용하기 위해 import
import { createSlice } from '@reduxjs/toolkit';
const boardReducer = createSlice({
name: 'boardReducer',
initialState: {
boards: [
{
boardId: 1,
boardTitle: '제목1',
boardContent: '내용내용내용1'
},
{
boardId: 2,
boardTitle: '제목2',
boardContent: '내용내용내용2'
},
{
boardId: 3,
boardTitle: '제목3',
boardContent: '내용내용내용3'
},
{
boardId: 4,
boardTitle: '제목4',
boardContent: '내용내용내용4'
},
{
boardId: 5,
boardTitle: '제목5',
boardContent: '내용내용내용5'
}
],
lastId: 5,
selectRowData: {}
},
reducers: {
boardSave: (state, { payload: saveData }) => {
if(saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...saveData, boardId: state.lastId+1 }), selectRowData: {} };
}
return { ...state, boards: state.boards.map(data => data.boardId === saveData.boardId ? {...saveData}: data), selectRowData: {} };
},
boardRemove: (state, { payload: boardId }) => {
return { ...state, boards: state.boards.filter(row => row.boardId !== boardId) };
},
boardSelectRow: (state, { payload: boardId }) => {
return {...state, selectRowData: state.boards.find(row => row.boardId === boardId) };
}
}
});
// boardSave, boardRemove, boardSelectRow Action 을 외부에서 dispatch 할 수 있게 export
export const { boardSave, boardRemove, boardSelectRow } = boardReducer.actions;
// reducer export
export default boardReducer.reducer;
위 코드를 토대로 reducers 의 boardSave 를 다른 컴포넌트에서 아래와 같이 dispatch 했다고 가정해본다.
const onSave = (saveData) => dispatch(boardSave(saveData));
이 때 reducers 의 boardSave 구절을 수행하게 되고,
여기서 인자 값으로 집어넣은 saveData 는 payload 에 들어가게 된다.
그 후 해당 로직을 수행한 뒤 state 를 return 해주는 것이다.
추가로 덧붙여 dispatch 할 때 인자를 1을 넣었는데
받아주는 reducers 부분에서 { payload: saveData } 가 아니라 { payload } 로 받는다면
saveData 가 아닌 payload 그 자체에 1 이 들어가게 되니 payload 를 그대로 로직에서 사용하면 되고,
위 처럼 { payload: saveData } 로 받으면 인자로 넘겨준 1 은 { saveData: 1 } 의 형태로 받아진다.
이럴 땐 위처럼 saveData 를 로직에 사용하면된다.