Redux-Toolkit ?
redux-toolkit 은 redux 를 보다 편리하게 사용하기 위해 제공되어 지는 redux 개발 도구이다.
이는 redux-thunk 를 기반으로 사용하지만, 그렇다고 해서 redux-saga 등 다른 미들웨어를 사용하지 못하는 건 아니다.
Redux-Toolkit 기능 중 자주 사용되는 몇 가지만 살펴보자.
이전 포스팅의 코드를 기준으로 들어 설명하긴 했으나 현 포스팅만 봐도 무방하다.
Redux-Toolkit 설치 ( 해당 project 경로에서 해야 함 )
// NPM
npm install @reduxjs/toolkit
// Yarn
yarn add @reduxjs/toolkit
configureStore
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
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 }
createReducer
일반적으로 기존에 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 해주는 셈이다.
createSlice
방금 위에서 다룬 리듀서는 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 를 로직에 사용하면된다.
특징 : 불변성 관리
이는 기능이라기 보다 부가적인 성능이다.
우리는 리액트를 하면서 불변성을 유지를 위해 push, splice 같은 기존 data 에 영향이 가는 메소드 대신
기존 state 등의 구조를 복사해 새로 생성해내는 concat 등의 메소드를 이용하길 강요받아왔다.
하지만 Redux-Toolkit 의 createReducer 와 createSlice 함수는
이러한 불변성까지 자동으로 관리해주는 유틸을 가지고 있다.
이는 createReducer 와 createSlice 는 immer 라이브러리를 내재하고 있기 때문인데
여기서 말하는 immer 란 우리가 불변성을 신경쓰지 않아도 불변성 관리를 알아서 대신 해주는 라이브러리이다.
즉, 우리는 더이상 리듀서에서 새로운 state 객체를 만들어 return 할 필요가 없어지고 state 를 직접 변경해도 된단 말이다.
먼저 기존엔 어떻게 했는지 위의 소스에서 reducers 코드 블럭 중 boardSave 부분을 예로 들면 아래와 같은데,
reducers: {
boardSave: (state, { payload: saveData }) => {
if(saveData.boardId === '') {
return { lastId: state.lastId+1, boards: state.boards.concat({ ...saveData, boardId: state.lastId+1 }), selectRowData: {} };
}
// ... 생략
},
// ... 생략
}
보이듯이 concat 을 사용해 새로운 state 객체를 리턴했다.
하지만 이럴 필요없이 아래처럼 해주어도 자기가 알아서 불변성 관리를 해 새로운 state 객체의 형태를 이루어주게 한다.
reducers: {
boardSave: (state, { payload: saveData }) => {
if(saveData.boardId === '') {
const { boardTitle, boardContent } = saveData;
// push 사용
state.push({
lastId: state.lastId+1,
selectRowData: {},
boards: [
{
boardId: state.lastId+1,
boardTitle: boardTitle,
boardContent: boardContent
}
]
});
}
// ... 생략
},
// ... 생략
}
특징 : Scope
만약 아래와 같은 Reducer 가 있다고 가정해보자.
export default function testReducer(state=initialState, action) {
switch(action.type) {
case A_COLOR:
let color='red'; //중복
return;
case B_COLOR:
let color='blue'; //중복
return;
case C_COLOR:
let color='yellow'; //중복
return;
default:
return state;
}
}
실행하면 변수 명이 스코프내에 중복된다고 error 를 띄울 것이다.
이는 reducer 가 기본적으로 함수 자체를 통으로 scope 로 잡기 때문이다.
반면 createReducer 와 createSlice 는
각 action 타입마다 코드블럭을 scope 로 잡기 때문에 변수를 scope 단위로 사용할 수 있다.
// createReducer example
export default createReducer(initialState, {
[A_COLOR]: (state) => {
let color='red';
return;
},
[B_COLOR]: (state) => {
let color='blue';
return;
},
[B_COLOR]: (state) => {
let color='yellow';
return;
}
})
// createSlice example
const colorReducer = createSlice({
name: 'colorReducer',
initialState: [],
reducers: {
aColor: (state) => {
let color='red';
},
bColor: (state) => {
let color='blue';
},
cColor: (state) => {
let color='yellow';
}
}
})
이상 redux-toolkit 의 소개를 마친당.
'Coding Story > REACT' 카테고리의 다른 글
[ React ] 리액트 React-Hook-Form (2) | 2020.10.27 |
---|---|
[ React ] 리액트 Saga + Toolkit ( 미들웨어 사가, 리덕스 툴킷 ) (1) | 2020.10.26 |
[ React ] 리액트 Redux 로 게시판 만들어보기 (8) | 2020.10.22 |
[ React ] 리액트 리덕스(Redux) 사용해보기 (0) | 2020.10.22 |
[ React ] 리액트 리덕스(Redux)란? (0) | 2020.10.22 |