상태관리 라이브러리/Redux

[Redux] 사용법부터 이론까지 살펴보는 Redux: 5. 입문자를 위한 createSlice 기본 세팅 소개

tea-tea 2024. 3. 20. 22:25

redux는 정말 좋은 라이브러리지만, 세팅해야할 게 많다. 액션 타입을 상수로 지정하고, 액션 생성자도 만들고, 액션 타입에 맞춰서 리듀서 함수도 만들어야 하니 말이다. 다행히 redux-toolkit은 이런 반복되는 코드를 정리해주는데, 그 중 createSlice는 createAction이나 createReducer에 비해서 간결한 코드를 제공하지만, 보일러 코드를 많이 축약하다 보니 처음 봐선 구조가 쉽게 이해되지 않는다. 따라서, 이 기능의 세팅방식에 대해 살펴보겠다. 

 

1. store 세팅

export const store = configureStore({
  reducer: { },
});
export type TypeStore = ReturnType<typeof store.getState>;

store는 cofigureStore을 사용하는 게 일반적이다. 이 기능을 사용하면 redux-dev-tool을 사용할 수 있기 때문에 디버깅이 편리해진다. 

reducer 부분은 좀 있다가 채워넣을테니 일단 비워놓자.

TypeStore은 store의 구조를 알려주기 위한 타입선언이다. 이건 리엑트 컴포넌트에서 useSelector로 state를 가져올 때 연결해주면 아주 유용하다.

 

2. react앱에 바인딩

import { Provider } from "react-redux";
import { store } from "./store";

const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(
  <Provider store={store}>
    <App />
  </Provider>
);

Provider을 가져와서 리엑트 앱의 최상단 컴포넌트에 감싸준다. 그리고 아까 만든 store을 연결해준다.

참고로, 리덕스에서는 단일 스토어를 사용하고 여러 개의 리듀서로 관심사를 분할하는 게 일반적이다.

 

3. slice 생성 및 연결

먼저 createSlice로 slice 객체를 생성한다. slice는 내부에서 action creater과 reducer를 알아서 생성해준다.

아래 부분에 작동 방식에 대한 주석과 함께 살펴보자.

//@ src/slice/counterSlice.ts

import { createSlice, PayloadAction } from "@reduxjs/toolkit";

const initialState = { value: 0 };

const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
  	
    // 아래 부분은 increment:(state)=>{state.value++}의 축약
    // 일반적인 reducer 함수에서 switch (action.type){ case: "counter/increasement"} 와 동일하게 작동한다
    increment(state) {
      state.value++;
    },
    decrement(state) {
      state.value--;
    },
    incrementByAmount(state, action:PayloadAction<number>) {
      state.value += action.payload;
    },
  },
});

// 액션 생성자
export const { increment, decrement, incrementByAmount } = counterSlice.actions;

// 리듀서
export default counterSlice.reducer;
// export type RootState = ReturnType<typeof counterSlice.reducer>;

 

slice에 이름을 지정하는 이유는 action type의 수식어로 사용되어서 다른 reducer의 action type과 중복되는 걸 막기 위함이다. 일종의 유니크한 id라고 생각하면 된다.

예컨대, 위 예시에서 increment 액션의 타입은 "counter/increment"가 된다.

리듀서 함수에서 액션 인자의 타입을 설정해줄 때는 PayloadAction<type>을 사용하면 된다.

 

 

액션 생성자는 일정한 형식의 액션 객체를 반환한다. 이들은 컴포넌트에서 dispatch 시에 개발자가 액션 형식을 기억할 부담을 덜어준다.

// 예시
const a = incrementByMount(10)
console.log(a)
// {type:"counter/incrementByMount", payload:10}

 

4. reducer를 store에 연결

만들어진 slice에서 reducer을 export하고, 이를 store에서 받아 연결해준다. 

import { configureStore } from "@reduxjs/toolkit";
import CounterReducer from "./slice/counterSlice";
import todoReducer from "./slice/todoSlice";

export const store = configureStore({
  reducer: { counter: CounterReducer, todo: todoReducer },
});
export type TypeStore = ReturnType<typeof store.getState>;

 

위 구조를 보면 어느정도 감이 오겠지만, 리덕스는 단일 스토어에서 대규모 앱의 복잡한 상태를 관리하기 위해 reducer를 단위로 state와 그 로직을 분리한다. 이 부분은 뒤의 useSelector을 보자.

 

5. 컴포넌트에서 useSelector로 state의 필요한 부분 가져오기

& useDispatch와 action creator로 상태 변경 요청하기

counter state를 컴포넌트로 가져와보자.

import { useDispatch, useSelector } from "react-redux";
import { TypeStore } from "./store";
import { decrement, increment, incrementByAmount } from "./slice/counterSlice";

export default function Counter() {
  console.log("counter");
  const counterState = useSelector((state: TypeStore) => state.counter.value);
  const dispatch = useDispatch();
  return (
    <div className="counter">
      <p>counter:{counterState}</p>
      <button onClick={() => dispatch(increment())}>add</button>
      <button onClick={() => dispatch(decrement())}>minus</button>
      <button onClick={() => dispatch(incrementByAmount(5))}>+5</button>
    </div>
  );
}

 

useSelector 부분에서 보이듯이, 단일 스토어 내에서 reducer인 counter를 단위로 다른 데이터(ex: todo)와 분리가 된다. 이런 분리는 리렌더링을 최적화하는데 도움이 된다.

state의 형식은 앱의 규모가 커질수록 개발자가 기억하기 어려우므로 아까 선언해둔 store의 타입을 가져오면 편리하다. 

dispatch 부분에서는 slice에서 만든 action creator을 가져오면 형식을 신경쓰지 않고 payload만 보내면 된다.