이미지09
Coding Story/REACT

[ React ] 리액트 리덕스 툴킷 redux-toolkit

반응형

 

 

 

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 의 소개를 마친당.

 

 

 

반응형