[React Redux] Redux Toolkit의 메서드 - (2)

Redux Toolkit의 다양한 메서드

리덕스 툴킷에는 리덕스를 더욱 편리하게 사용할 수 있도록 제공해주는 다양한 메서드들이 있다. 그 중 createReducer(), createAction(), createSlice()에 대해 알아보았다.

createReducer()

리덕스에서의 reduceraction의 타입과 해당 액션 타입에 따른 작동을 정의하여 switchcase로 관리해야 할 뿐만 아니라 immutive한 로직을 작성해야 했다. 이는 중첩이 깊어질 수록 코드가 길어지고 실수로 원본 객체를 변형시키는 위험성이 높아지는 등의 여러 문제를 유발하는데, 반면에 createReducer()는 mutative한 로직을 작성해도 자동으로 immutative한 업데이트가 이루어져 걱정이 없다. 또한 builder callback이라는 것을 활용하여 중첩을 더 효율적으로 해결할 수 있게 해준다.

const initialState = { value: 0 };

function counterReducer(state = initialState, action) {
  switch (action.type) {
    case "increment":
      return { ...state, value: state.value + 1 }; // immutative
    case "decrement":
      return { ...state, value: state.value - 1 }; // immutative
    default:
      return state;
  }
}

본래라면 이렇게 ... 연산자를 사용하여 코드를 작성해야 하지만, createReducer()를 사용한다면

import { createAction, createReducer } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const increment = createAction('counter/increment')
const decrement = createAction('counter/decrement')

const initialState = { value: 0 } as CounterState

const counterReducer = createReducer(initialState, (builder) => {
  builder
    .addCase(increment, (state, action) => {
      state.value++ // mutative
    })
    .addCase(decrement, (state, action) => {
      state.value-- // mutative
    })
})

builder callback으로 이렇게 작성할 수 있는 것이다.

Builder Callback

Builder Callback은 builder 객체 내의 addCase, addMatcher, addDefaultCase 메서드를 호출하여 리듀서의 액션에 대한 처리를 정의할 수 있게 해준다.

addCase(actionCreator, reducer)

액션 타입이 정확히 일치하면 reducer로 처리한다.

addMathcer(matcher, reducer)

액션에 대해 matcher의 패턴과 일치하는지 확인하고, 만약 일치한다면 처리한다. 여러 addMatcher들과 패턴이 일치한다면 작성된 순서에 맞춰 차례로 reducer로 처리한다.

addDefaultCase(reducer)

일치하는 액션 타입이 없는 경우 reducer로 처리한다.

createAction()

리덕스에선 actionaction creator를 따로 정의해야 했지만 createAction()은 이 두 가지를 결합한 형태이다.

const INCREMENT = "counter/increment";

function increment(amount) {
  return {
    type: INCREMENT,
    payload: amount,
  };
}

처럼 타입과 생성자를 분리해야 했던 것을

import { createAction } from "@reduxjs/toolkit";

const increment = createAction("counter/increment");

로 결합할 수 있다. 이 경우

let action = increment();
console.log(action); // { type: 'counter/increment' }

처럼 인자에 아무것도 전달을 하지 않은 상태로 호출할 시 타입이 반환되는 것을 확인할 수 있다. 또,

action = increment(10);
console.log(action); // { type: 'counter/increment', payload: 10 }

처럼 인자에 값을 전달하면 payload에 담기게 된다.

Prepare Callbacks

만약 createAction()으로 액션을 만들고, 그 액션에 인자 하나를 전달해서 payload를 생성하는 것이 아니라 인자에 따른 결과값으로 생성하고 싶다면 prepare 콜백 함수를 createAction()의 두 번째 인자로 전달하면 된다.

import { createAction } from "@reduxjs/toolkit";

const addTodo = createAction("todos/add", function prepare(text) {
  return {
    payload: {
      text,
      author: "Moon",
      createdAt: new Date().toISOString(),
    },
  };
});

let newTodo = addTodo("Code more");
console.log(newTodo);
// {
//   type: 'todos/add',
//   payload: {
//     text: "Code more",
//     author: "Moon",
//     createdAt: "2022-05-16T11:47:36:571Z"
//   }
// }

이렇게 동적으로 원하는 값을 담는 것도 가능하다.

createSlice()

Redux를 다루기 위해 Redux Toolkit에서 제공하는 가장 기본적이고 표준적인 접근 방법이다.
createSlice()는 이름, 초기값, reducer 함수를 담은 객체를 인자로 받아 자동적으로 액션 타입과 액션 생성자를 만들어 주는 메서드이다. 심지어 내부적으로 createAction()createReducer()가 사용되기 떄문에 직접 작성해줄 필요도 없다.

import { createSlice, PayloadAction } from '@reduxjs/toolkit'

interface CounterState {
  value: number
}

const initialState = { value: 0 } as CounterState

const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value++
    },
    decrement: (state) => {
      state.value--
    },
    addNumber: {
      reducer: (state, action: PayloadAction<number>) => {
        state += action.payload
      },
      prepare: (number: number) => {
        return { payload: number}
      },
      extraReducers: (builder) => {
        builder
          .addCase(multiply, (state, action) => {
            state *= action.payload
          })
      }
    }
  },
})

export const { increment, decrement } = counterSlice.actions
export default counterSlice.reducer

사용하고 싶은 액션을 export하고 그대로 사용하면 된다. 이 경우 incrementdecrement이다.

name

name: 'counter',

액션 타입이 생성될 때 / 앞에 붙게 될 이름이다. 즉, 고유성을 부여해 준다고 생각하면 된다. 예시에선 counter/incrementcounter/decrement가 되는 것이다.

initialState

initialState,

초기값을 의미한다.

reducers

reducers: {
  increment: (state) => {
    state.value++
  },
  decrement: (state) => {
    state.value--
  },
}

액션 타입에 따른 리듀서를 의미한다. 각각의 키 값은 해당 리듀서를 dispatch하기 위한 용도가 된다.

Prepare Callback

addNumber: {
  reducers: {
    reducer: (state, action: PayloadAction<number>) => {
      state += action.payload
    },
    prepare: (number: number) => {
      return { payload: number}
    }
  }
}

createSlice()에서도 prepare callback을 사용할 수 있다. reducer를 정의할 때 객체로 reducerprepare를 각각 정의해줌으로써 payload에 원하는 값을 전달할 수 있다.

extraReducers

extraReducers: (builder) => {
  builder.addCase(multiply, (state, action) => {
    state *= action.payload;
  });
};

createSlice()가 생성한 액션 타입 외에 다른 액션 타입도 처리할 수 있도록 정의해준다. 내부에 정의된 액션만이 아닌 외부 액션에도 반응할 수 있도록 하기 위한 방법이다.

참고: React Redux 공식 문서
참고: Redux Toolkit 공식 문서