8 분 소요

enter image description here

Toolkit이란?

redux-toolkit 은 redux 를 보다 편리하게 사용하기 위해 제공되어 지는 redux 개발 도구입니다. redux를 아무런 라이브러리 없이 사용할 때에는 필요하지만 너무 많은 코드가 생성됩니다. toolkit은 redux가 만든 공식적인 라이브러리로, saga를 제외한 많은 기능을 지원합니다.

리덕스의 상태 변화의 흐름

enter image description here

  • 진행 순서
      1. 버튼을 클릭합니다.
      1. 앱에서는 유저의 행동에 맞는 디스패치를 실행해 액션을 일으킵니다.
      1. 스토어에서서는 이전 상태와 현재 액션으로 리듀서 함수를 실행합니다. 그 리턴 값을 새로운 상태로 저장합니다.
      1. 슽토어는 스토어를 구독한 UI들에게 업데이트 되었다고 알려줍니다.
      1. 스토어의 테이터가 필요한 각각의 UI들은 필요한 상태 업데이트 되었는지 확인합니다.
      1. 데이터가 변경된 각 구성 요소를 새 데이터로 강제로 다시 랜더링 하므로 화면에 표시되는 내용을 업데이트 할 수 있습니다.

Toolkit 설치

# NPM
npm install @reduxjs/toolkit
# Yarn
yarn  add @reduxjs/toolkit

Toolkit의 기능

  • 기능의 종류
    • createAction
    • createReducer
    • configureStore
    • createSlice

createAction


createAction은 action을 보다 간결하게 만들어 줄 수 있게 도와줍니다. 먼저 createAction없이 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 를 생성해줍니다. 그리고 redux-tookit은 어떤 정보를 출력하든지 혹은 전송하든지간에 payload에 담겨 함께 보내집니다.

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;
  }
}

위의 reducer 코드를 createReducer를 사용하여 swith 와 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 문이 사라졌습니다. createReucer 의 첫번 째 인자값인 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() 메소드를**오버라이드 했기 때문입니다**.

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

불변성과 scope

  • 불변성 관리 - 리액트를 이용하면서 불변성의 유지를 위해 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";
    },
  },
});

댓글남기기