만족

[React] redux-toolkit 을 알아보자 본문

[React] redux-toolkit 을 알아보자

FrontEnd/React Satisfaction 2022. 2. 25. 15:45

redux에 대해 포스트를 작성하고, 이후 redux-toolkit에 대해 작성해보겠다 하고는 시간이 꽤나 지났다.

 

https://satisfactoryplace.tistory.com/278

 

[Nodejs] Redux의 기본 이해

https://redux.js.org/tutorials/fundamentals/part-1-overview Redux Fundamentals, Part 1: Redux Overview | Redux The official Fundamentals tutorial for Redux: learn the fundamentals of using Redux red..

satisfactoryplace.tistory.com

 

이번에 회사에 입사하게 되었는데, 마침 상태관리 도구로 redux-toolkit을 사용하고 있길래 공부 겸 정리해보려 한다.

 

redux에 대해 아예 모른다면 위 글이나 redux 공식 가이드를 읽고 오는 것을 추천한다.

 

redux-toolkit은 무엇인가?

 

redux-toolkit은 기존 redux의 여러 문제를 해결한 버전의 모듈이다.

 

redux와 본질은 동일하고, 더 사용하기 편하게 개량한 버전이라고 보면 되겠다.

 

redux에서는 크게 다음의 3가지 문제가 있다고 한다.

 

"Configuring a Redux store is too complicated"
"I have to add a lot of packages to get Redux to do anything useful"
"Redux requires too much boilerplate code"

리덕스 스토어를 설정하기가 너무 복잡하다
리덕스를 유용하게 사용하기 위해 너무 많은 패키지들이 필요하다
리덕스는 너무 많은 보일러플레이트 코드를 요구한다

 

2가지 이상의 상태 관리 도구를 사용해봤다면, 

유독 리덕스의 코드가 많고 복잡하다는 것을 경험했을 것이다.

 

redux-toolkit은 그것을 어느정도 완화시켰다.

 

설치

#react project 생성
npx create-react-app redux-example

#redux 설치
npm install @reduxjs/toolkit react-redux

 

가보자고 ㅋㅋ

 

사용해보기: 리덕스 스토어 설치

먼저 리덕스 스토어를 만들 것이다.

 

// store.js

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

//redux store
export const store = configureStore({
  reducer: {},
});

reducer가 포함된 객체를 configureStore에 전달하면 간단히 redux store가 만들어진다.

 

이것을 react에 적용하기 위해 index.js로 이동해서

 

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

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

ReactDOM.render(
  //이곳에서 아래와 같이 Provider에 store를 전달하고 App을 감싼다
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

//...

하면 준비는 끝났다.

 

사용해보기: Slice 만들기

기본 redux로는 리듀서를 만들 때 switch를 이용해 액션을 구분하여 상태를 변화시켰다.

 

또한 디스패치를 호출할 때도 구조화된 액션을 매개변수로 넣어줘야만 했다.

 

toolkit에서는 이러한 복잡한 정의/사용을 탈피하고자 createSlice라는 새로운 API를 지원한다.

 

// /redux/counter.js

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

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

export const { increment, decrement } = counterSlice.actions;

export default counterSlice.reducer;

createSlice에서는 액션이 디스패치되었을 때의 동작을

'switch'가 아닌 'object'형태로 정의할 수 있다.

// 이전의 reducer 생성 방법

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

이전에는 위와 같이 스위치로 액션을 구분해 동작을 정의해야만 했다.

 

딱 보기에도 toolkit보다 직관성이 떨어졌고,

action.type을 string으로 하나하나 직접 구분해야 하는 점도 마음에 들지 않는다.

 

toolkit이 뭔가 마법을 부린 것이 아니라, redux에다 단순히 추상화 레이어를 한 단계 추가하여

동일하게 작동하되 다른 방법으로 사용할 수 있게 한 것이다.

 

https://redux-toolkit.js.org/api/createSlice

 

createSlice | Redux Toolkit

 

redux-toolkit.js.org

 

뭔가 오리지널 리덕스를 까는 곳으로 빠졌는데, 다시 toolkit으로 돌아오자.

 

// /redux/counter.js

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

const initialState = {
  value: 0,
};

export const counterSlice = createSlice({
  //name은 각 action에 대한 prefix
  name: "counter",
  //초기값
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
  },
});

//actions
//dispatch할 때 액션을 전달해 상태를 어떻게 변화시킬지를 결정한다
export const { increment, decrement } = counterSlice.actions;

//reducer
export default counterSlice.reducer;

 

createSlice로 slice를 만들면, slice에 reducer, actions가 생성되어 외부에서 사용할 수 있게 된다.

 

counter에 대한 준비는 끝났으니, 다시 store로 돌아가 reducer에 counter를 추가해준다.

 

// store.js

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

//방금 전 counter.js에서 export default한 counterSlice.reducer
import counterReducer from "./redux/counter";


//redux store
export const store = configureStore({
  reducer: {
    //add counterReducer
    counter: counterReducer
  },
});

이제 counter에 대해 increment/decrement를 사용할 수 있다.

 

사용해보기: Counter 컴포넌트 생성

// /components/Counter.js

import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { decrement, increment } from "../redux/counter";

const Counter = () => {
  //useSelector를 통해 redux store에서 특정 값을 관찰할 수 있다
  const count = useSelector((state) => state.counter.value);
  
  //dispatch에 action을 전달하면 해당 동작이 실행된다
  const dispatch = useDispatch();

  //increment(1증가) 동작
  const handleIncrement = () => dispatch(increment());
  //decrement(1감소) 동작
  const handleDecrement = () => dispatch(decrement());
  
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleIncrement}>increment</button>
      <button onClick={handleDecrement}>decrement</button>
    </div>
  );
};

export default Counter;

사실 여기는 toolkit에 새로 생긴 기능은 아니라서 별로 설명할 것은 없는 것 같다.

 

useSelector는 reduxt store의 특정 값을 후킹해오는 함수이고,

dispatch는 리덕스 스토어에 액션을 전달하기 위한 함수이다.

 

제대로 동작하는지 검사하기 위해 Counter컴포넌트를 만들었고,

얘를 App.js에도 포함시켜 테스트해볼 것이다.

 

// App.js

import "./App.css";
import Counter from "./components/Counter";

function App() {
  return (
    <div className="App">
      <Counter />
    </div>
  );
}

export default App;

 

 

잘된당 ㅎㅎ

 

사용해보기: 미들웨어 추가하기

 

액션이 리듀서에 전달되기 전에 middleware를 설치하여 인터셉트할 수 있다.

 

redux-thunk, redux-saga 같은 라이브러리가 그것인데,

여기에서는 간단한 미들웨어를 통해 어떻게 미들웨어를 추가하는지 살펴보자

// store.js

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./redux/counter";

//new middleware
const loggerMiddleware = (store) => (next) => (action) => {
  console.group(action.type);
  console.info("dispatching", action);
  console.log("before state", store.getState());

  //액션을 다음 미들웨어 또는 리듀서에 전달
  let result = next(action);
  
  //next(action)후 state는 정의된 동작에 따라 변한다
  //next(action)없이 return store.getState()하면 상태는 변하지 않는다
  console.log("next state", store.getState());
  
  console.groupEnd();

  return result;
};

//redux store
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },

  //concat을 통해 미들웨어를 추가한다
  //여러개일 경우 반복한다
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware().concat(loggerMiddleware),
});

store.js (configureStore)부분에서 getDefaultMiddleware().concat()을 통해 middleware를 추가할 수 있다.

 

여기에서는 값을 거르거나, 변형하는 것이 목적이 아니라 로깅이므로, 항상 next(action)하여 

다음 미들웨어나 리듀서로 진행한다.

 

loggerMiddleware 덕분에 스토어의 상태 변화와, 액션에 대해 콘솔에 출력할 수 있게 되었다.



Comments