redux-toolkit

ami_jusun·2022년 3월 10일
0
post-thumbnail

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 를 로직에 사용하면된다.

profile
쓸모없는 도전은 없으니까

0개의 댓글