React) redux toolkit를 사용해봤다 (with. TS)

2ast·2022년 10월 13일
0

지난번 recoil에 이어 이번에는 redux toolkit을 사용해봤다. 직접 사용해보기에 앞서 관련 서치를 진행했는데, 기존 redux와 비교해서 필수 패키지 수가 많이 줄었고 단계도 많이 줄이려고 노력했다는게 보였다. 그럼에도 사전 셋팅이 거의 불필요하다시피한 recoil이나 zustand에 비해서는 여전히 허들이 있다고 느껴지는 것은 어쩔 수 없는 것 같다.

처음에는 다른 사람들이 쓴 블로그글을 보고 시작하려했지만, 정작 공식문서가 가장 깔끔한 것 같아 공식문서를 보고 셋팅을 시작했다.

https://redux-toolkit.js.org/tutorials/quick-start

그리고 사실 공식문서의 코드들을 읽지도 않고 그냥 무지성으로 복사 붙여넣기만 해도 counter 예제는 쉽게 구현 가능하기에, 이를 응용해서 user state를 따로 만들어 보기로 했다.

redux-toolkit 돌입

일단 설치부터 하고 시작!

npm install @reduxjs/toolkit react-redux

store.tsx 셋팅

import { configureStore } from "@reduxjs/toolkit";
import counterReducer from "./features/counter/counterSlice";
import userReducer from "./features/user/userSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
    user: userReducer,
  },
});


export type RootState = ReturnType<typeof store.getState>;

export type AppDispatch = typeof store.dispatch;

store 파일은 이것으로 셋팅이 완료되었다. 기존에는 configureStore가 아닌 createStore로 만들었다고 하는데, 그때는 서드파티 라이브러리를 설치해 추가로 설정해줘야했던 부분들이 redux-toolkit에서는 기본값으로 설정되어 있기 때문에, reducer만 설정해줘도 바로 사용할 수 있다고 한다. 또한 reducer들을 넣어줄때도 굳이 묶어줄 필요없이 저런식으로 object 형태로 입력하면 된다.

Root Component 설정

redux toolkit을 사용하기 위해서는 루트 컴포넌트를 Provider로 감쌀 필요가 있다.

//index.tsx

import ReactDOM from "react-dom/client";
import App from "./App";
import { store } from "./app/store";
import { Provider } from "react-redux";

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

slice만들기

redux-toolkit에서는 slice라는 개념이 도입되었는데, action과 reducer를 한번에 생성할 수 있게 해주는 역할을 한다고 한다. 기존 redux에서는 모두 각각 작성해야 했기에 slice덕분에 코드량이 획기적으로 줄어들었다고 한다.

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

export interface UserState {
  name: string;
  age: number;
}

const initialState: UserState = {
  name: "kim",
  age: 15,
};

export const userSilce = createSlice({
  name: "user",
  initialState,
  reducers: {
    editName: (state, action: PayloadAction<string>) => {
      state.name = action.payload;
    },
    editAge: (state, action: PayloadAction<number>) => {
      state.age = action.payload;
    },
  },
});

export const { editName, editAge } = userSilce.actions;

export default userSilce.reducer;

slice를 만들때 필수적으로 입력해야하는 필드는 name,initialState,reducers이다. 여기서 중요한 것은 reducers인데, value로 함수를 담고 있는 object형태로 되어 있다. 여기서 key는 action name이 될것이고, value는 action이 호출되었을 때 실행될 reducer 로직을 담고 있다. 인수로 state와 action을 갖고 있는데, state는 말 그대로 state를 의미하고, action을 통해 호출 시점에 함께 날린 payload에 접근할 수 있다.

이제 이렇게 생성한 slice를 export하기만 하면 되는데, 코드를 보면 알겠지만 actions와 reducer를 각각 export하고 있다. 이는 reducer와 actions가 사용되는 곳이 다르기 때문이다. reducer는 store를 구성하는데 필요하기에 store에서 import하게 되고, actions는 실제 컴포넌트에서 import하여 dispatch로 reducer를 호출할 때 쓰인다.

여담

여담으로, 코드를 보면 reducer에서 어떠한 값을 return하는 것도 아니고 state 값을 직접 변경하는 것처럼 구현되어 있는데, 공식문서 코드에는 여기에 대해 주석을 달아 설명하고 있다.

Redux Toolkit allows us to write "mutating" logic in reducers. It doesn't actually mutate the state because it uses the Immer library, which detects changes to a "draft state" and produces a brand new immutable state based off those changes

간단히 번역하자면, 직접 state 값을 수정하는 것처럼 보이지만, state는 불변성을 유지해야하기에 이는 불가능하고, 실제로는 입력된 값을 기반으로 새로운 state를 만드는 것이라고 한다.

컴포넌트에서 사용하기

이제 기본 셋팅과 userState 설정이 끝났으니 실제 프로젝트에서 사용할 일만 남았다.

//App.tsx
import { useSelector, useDispatch } from "react-redux";
import { editName, editAge } from "./app/features/user/userSlice";
import { RootState } from "./app/store";

function App() {
  const { name, age } = useSelector((state: RootState) => state.user);
  const dispatch = useDispatch();
  

  const changeName = (newValue: string) => {
    dispatch(editName(newValue));
  };

  const changeAge = (newValue: number) => {
    dispatch(editAge(newValue));
  };

  ...
  
}

useSelector를 사용하면 state를 가져올 수 있다. (recoil에서 selector사용은 옵션이었는데, redux에서는 이게 기본 사용법인가보다.) useSelector 내부 콜백은 state를 받아 값을 반환하는데, 이때 state의 type은 store에서 export한 RootState 타입을 사용하면 된다. 그리고, state 매개변수를 통해 원하는 값에 접근하는 방법은 state.[reducer이름]을 쓰면 된다.

state를 변경하는 방법도 나름 간단한데, 아래와 같은 형태로 dispatch안에 action과 payload를 주면 된다.

dispatch(actionName(payload));

이제 결과물을 보자

name과 age가 있는 userState를 만들었다. 이 state를 UI와 연동시켜 잘 동작하는지 확인해보려고 한다.

import React, { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { editName, editAge } from "./app/features/user/userSlice";
import { RootState } from "./app/store";

function App() {
  const { name, age } = useSelector((state: RootState) => state.user);
  const dispatch = useDispatch();

  const [text, setText] = useState("");

  const changeName = (newValue: string) => {
    dispatch(editName(newValue));
  };

  const changeAge = (newValue: number) => {
    dispatch(editAge(newValue));
  };

  const onClickHandler = (inputText: string) => {
    if (isNaN(Number(inputText))) {
      changeName(inputText);
    } else {
      changeAge(Number(inputText));
    }
    setText("");
  };

  return (
    <div>
      <div>
        {name} {age}
        <input value={text} onChange={(e) => setText(e.target.value)} />
        <button
          disabled={text.length === 0}
          onClick={() => onClickHandler(text)}
        >
          이름 바꾸기
        </button>
      </div>
    </div>
  );
}

export default App;

하나의 input이 있고, 이름 바꾸기 버튼을 눌렀을 때 입력된 값이 number 타입이라면 이름을 수정하고, 입력된 값이 string타입이라면 name을 수정하는 코드다.

잘 동작한다.

소감

이렇게 redux toolkit을 처음 써봤는데, 확실히 마냥 쉽다고만은 할 수 없을 것 같다. 아무래도 기존 redux의 단점을 보완해서 더 라이트하게 나온 버전이라 그런지 redux toolkit에 대해 알아보려고 해도 redux에서 쓰이던 개념이나 기능들에 대해 먼저 알고 있어야 이해가 수월한 부분들이 많기도 하고, slice같이 아예 새로운 개념이 등장하기도 하기 때문이다.

하지만 그렇다고 또 마냥 어렵냐하면 그건 아니다. 공식문서의 기본 예제를 만들어 보고 모르는 개념들은 조금씩 구글링 하다보니 1시간도 안되어서 기본 사용법 정도는 충분히 숙지할 수 있게 된 것 같다. 요즘 상태관리 라이브러리가 워낙 쉽게 나오다보니 상대적으로 어려워 보일 수는 있는데, 객관적으로 이게 정말 어려운 개념인가? 라고 하면 그정도는 아니라는게 내 소감이다.

하지만 지금까지 해본것은 어디까지나 기본 사용법이다 보니, 앞으로 조금씩 더 만져보면서 redux toolkit의 기능들을 하나하나 체험해볼 예정이다. 그리고 만약 여건이 허락된다면 redux toolkit을 사용한 작은 토이프로젝트도 하나 해볼 생각이다.

profile
React-Native 개발블로그

0개의 댓글