programming language/react

[React] redux말고 Redux-toolkit 사용하기 + redux-saga 병합

공대키메라 2022. 7. 9. 21:19

전번에 필자가 redux-saga를 공부했었는데,

 

이런 문제가 있다.

 

 

redux로 이제 store를 생성해서 사용 했는데

 

이게 deprecated되어서 redux-toolkit을 사용하라고 버젓이 적혀있다.

 

이것을 또 놓치고 그냥 가기에는 키메라의 마음에 너무 걸린다 끼에엑!@

 

고로... 이번 시간에는 redux-toolkit에 대해 알아보고 이것을 어떻게 기존 코드를 변경시켜서 

 

redux team에서 추천하는 redux-toolkit으로 도배할 것이다 (가즈아!)

 

참고한 사이트는 다음과 같다.

 

출처

https://www.youtube.com/watch?v=9MMSRn5NoFY&ab_channel=EdRoh 

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

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

 

 

1. Redux-Toolkit의 의도

사실 redux는 react와 무관한 독립적인 라이브러리이다.

 

react의 경우 데이터는 한방향으로 흐르기 때문에, 이 데이터를 다루는 것이 굉장히 힘든 작업이었다.

 

이러한 불편함을 해결하기 위해 redux를 사용하는 것인데 기존에 redux를 사용할 때 문제가 있었다.

 

공식 사이트에서는 redux-toolkit의 목적을 다음과 같이 설명한다. 

redux toolkit 패키지는 redux 로직을 사용하는 기준 방식이 되도록 의도한다.

이것은 redux에 대한 세개의 흔한 걱정을 고심하려고 만들어졌다.

1. redux store를 설정하는게 너무 복잡하다
2. 유용한 redux를 가지는 패지키가 너무 많다!
3. redux는 너무 많은 boilerplate 코드가 필요하다.

 

이게 해보면 그렇게 복잡한 것 같지는 않는데...그렇다고 한다! 더 편해지면 좋지!

 

그러면 기존의 redux와 다른 것이 무엇인가?

 

공식 사이트에서 쉽게 새롭게 추가된 내용에 대해 알아볼 수 있다.

새롭게 추가된 기능

리덕스 툴킷은 다음 API를 포함한다.

configureStore(): 간소화된 설정 옵션과 좋은 기본값으로 제공하기 위해 createStore를 감싼다. 자동적으로 slice reducer들을 결합할 수 있고 어떤 redux 미들웨어던지 추가가 가능하다. redux-thunk는 기본적으로 포함되어있고, 리덕스 데브툴스 익스텐션(Redux DevTools Extention)사용이 가능하다.

createReducer(): switch 를 사용하는 것 보다 오히려 reducer 함수를 action 타입의 경우에 lookup table을 제공한다. 자동적으로 immer 라이브러리를 사용한다. 

createSlice() : 리듀서 함수의 객체, slice name, initial state value를 받고, 자동적으로 action creator와 action type에 응답하는 slice reducer를 생성한다.  

createAsyncThunk : 프로비스를 반환하는 함수와 action type string 을 받는다. 그리고 프로미스 기반의 pending/fulfilled/rejected action 타입들을 dispatch한다. 

createEntityAdapter : 재사용가능한 리듀서들과 셀렉터들을 생성한다. 

 

immer를 사용해서 불변성을 유지할 수 있지만, 이제는 createReducer를 사용하면 이미 불변성이 유지가 된다니 유용한 기능이 추가가 되었다. 

 

그리고 createReducer를 하게되면 기존에는 switch로 action type을 일일이 분리해서 분기문을 생성했는데 그에 대해서도 무언가 기능을 제공하는 것 같다. 

2. redux-toolkit 실습 프로젝트 생성


 youtube에서 찾앗는데 영어권 분들이 찾아볼 거리도 많고 너무 속이 편안했다.

 

디렉토리와 파일 이름도 안알려주는 블로그 내용이지만 최상단에 노출이 되고, 찾아보지 않고 아 되네? 하고 그냥 사용한 것을 올린 사람들 때문에 나같은 코린이가 눈물을 흘린다... ㅠㅠ


 

npx create-react-app redux-toolkit-practice-with-saga

 

원하는 디렉토리에 명령어 실행

 

그리고 @reduxjs/toolkit와 react-redux를 다운받는다. 

 

npm i @reduxjs/toolkit react-redux

 

@reduxjs/toolkit, react-redux 설치

 

실습 디렉토리는 다음과 같이 나눈다.

 

 

boilder plate 로 다운받고 바로 사용한건 아니고 자동적으로 다운되는 불필요한 파일을 삭제했다.

src/App.js

import { useSelector } from "react-redux";
import LoginForm from "./Login/LoginForm";
import Profile from "./Login/Profile";

function App() {
  const { loginDone } = useSelector((state) => state.user);
  return <>{loginDone ? <Profile /> : <LoginForm />}</>;
}

export default App;

src/store/store.js

import { configureStore, applyMiddleware } from "@reduxjs/toolkit";
import { userReducer } from "../reducers/user";
import createSagaMiddleware from "redux-saga";
import rootSaga from "../saga/index";
import logger from "../Logger/MyLogger";

const saga = createSagaMiddleware();
const store = configureStore({
  reducer: {
    user: userReducer,
  },
  middleware: [logger, saga],
});

saga.run(rootSaga);

export default store;

 

여기서 configureStore에 대해 위에서도 설명했지만 combineReducer는 필요가 없다. 

 

애초에 내장이 되어있기에, 내부에 그냥 잘 넣어주면 알아서 잘 병합해준다. 

src/saga/index.js

import { all, fork } from "redux-saga/effects";
import userSaga from "./user";

export function* rootSaga() {
  yield all([fork(userSaga)]); // all 은 배열 안의 여러 사가를 동시에 실행시켜줍니다.
}

export default rootSaga;

saga도 사실 사용을 하면 여러가지 기능에 관련된 사가가 있을 텐데, 예시라던지, 기존의 코드들은 예시라서 그렇게 나누지를 않았다. 

 

하지만 필자 키메라는 좀 더 정성을 들여서 나누어봤다.\

src/saga/user.js

import { all, delay, fork, put, takeLatest } from "redux-saga/effects";
import {
  LOG_IN_FAILURE,
  LOG_IN_SUCCESS,
  LOG_OUT_FAILURE,
  LOG_OUT_SUCCESS,
} from "../reducers/user";

function* logIn(action) {
  try {
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put(LOG_IN_SUCCESS());
  } catch (err) {
    yield put(LOG_IN_FAILURE());
  }
}

function* logOut(action) {
  try {
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put(LOG_OUT_SUCCESS());
  } catch (err) {
    yield put(LOG_OUT_FAILURE());
  }
}

function* watchLogIn() {
  console.log("watchLogIn");
  yield takeLatest("user/LOG_IN_REQUEST", logIn);
}

function* watchLogOut() {
  yield takeLatest("user/LOG_OUT_REQUEST", logOut);
}

export default function* userSaga() {
  yield all([fork(watchLogIn), fork(watchLogOut)]);
}

기존에 사용하던 예시와 좀 다른것이 있다.

 

비교하면서 보면 이해가 더 잘 될 것이다. (여기 클릭!)

 

들어가는 것이 귀찮다면 괜찮다. 키메라가 한번 다시 가져오겠다! 끼에에엑!

src/saga/user.js - 구버전

import { all, delay, fork, put, takeLatest } from "redux-saga/effects";
import {
  LOG_IN_FAILURE,
  LOG_IN_REQUEST,
  LOG_IN_SUCCESS,
  LOG_OUT_FAILURE,
  LOG_OUT_REQUEST,
  LOG_OUT_SUCCESS,
} from "../reducers/user";

function* logIn(action) {
  try {
    console.log("saga logIn");
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put({
      type: LOG_IN_SUCCESS,
      data: action.data,
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOG_IN_FAILURE,
      error: err.response.data,
    });
  }
}

export function* logOut(action) {
  try {
    console.log("saga logOut");
    // const result = yield call(logInAPI);
    yield delay(1000);
    yield put({
      type: LOG_OUT_SUCCESS,
    });
  } catch (err) {
    console.error(err);
    yield put({
      type: LOG_OUT_FAILURE,
      error: err.response.data,
    });
  }
}

export function* watchLogIn() {
  console.log("watchLogIn");
  yield takeLatest(LOG_IN_REQUEST, logIn);
}

function* watchLogOut() {
  yield takeLatest(LOG_OUT_REQUEST, logOut);
}

export default function* userSaga() {
  yield all([fork(watchLogIn), fork(watchLogOut)]);
}

위의 구버전 예시는 immer를 적용하지 않았다. 

 

그런데 새로운 reduxjs/toolkit을 사용하면 장점이 많아 보인다.

 

각각 action에 관해서 전부 이름도 export해주고, 생성해서 그것을 받아서 사용해야 했다. 또한, 불변성을 유지하기 위해서 

spread 연산자를 통해서 일일이 해주는 것이 싫어서 immer라는 라이브러리를 별도로 설치를 해야 했다.

 

하지만, reduxjs/toolkit에서는 위에 언급된 문제들을 전부 해결해준다. (속이 편안...)

 

차이를 잘 보면, 단순 action이름만 변경하는 것인지, reduxjs.toolkit에서 생성한 action에 대해 함수처럼 접근하는지 차이가 있다.

 

비교를 해보면서 정확히 인지를 하길 바란다. 

src/Login.LoginForm.js

import { Button, Checkbox, Form, Input } from "antd";
import "antd/dist/antd.css";
import { useDispatch, useSelector } from "react-redux";
import { LOG_IN_REQUEST } from "../reducers/user";

const LoginForm = () => {
  const dispatch = useDispatch();
  const { loginHandling } = useSelector((state) => state.user);

  const onFinish = (values) => {
    dispatch(LOG_IN_REQUEST(values));
  };

  return (
    <Form
      name="basic"
      style={{ width: 300 }}
      initialValues={{
        remember: true,
      }}
      onFinish={onFinish}
      autoComplete="off"
    >
      <Form.Item
        label="Username"
        name="username"
        rules={[
          {
            required: true,
            message: "Please input your username!",
          },
        ]}
      >
        <Input />
      </Form.Item>

      <Form.Item
        label="Password"
        name="password"
        rules={[
          {
            required: true,
            message: "Please input your password!",
          },
        ]}
      >
        <Input.Password />
      </Form.Item>

      <Form.Item
        name="remember"
        valuePropName="checked"
        wrapperCol={{
          offset: 8,
          span: 16,
        }}
      >
        <Checkbox>Remember me</Checkbox>
      </Form.Item>

      <Form.Item
        wrapperCol={{
          offset: 8,
          span: 16,
        }}
      >
        <Button type="primary" htmlType="submit" loading={loginHandling}>
          Submit
        </Button>
      </Form.Item>
    </Form>
  );
};

export default LoginForm;

src/login/Profile.js

import { Button } from "antd";
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { LOG_OUT_REQUEST } from "../reducers/user";
const profile = () => {
  const dispatch = useDispatch();
  const { loginHandling } = useSelector((state) => state.user);
  const tryLogout = useCallback(() => {
    dispatch(LOG_OUT_REQUEST());
  });
  return (
    <>
      <div>profile</div>
      <Button onClick={tryLogout} loading={loginHandling}>
        로그아웃 하기
      </Button>
    </>
  );
};

export default profile;

 

LoginForm.js 와 Profile.js는 기존에 사용했던 코드와 다른게 크게 없다.

 

다만 전에 언급했듯이, action을 함수형태로 dispatch하는 것이 다르다. 

 

위의 코드를 넣고 실행하면 기존과 동일하게 작동하는 프로젝트를 확인할 수 있다.

 


사실 키메라는 기존의 강의를 많이 참고하면서, 내 입맛에 맞게 정보를 더 찾아보고, 그것을 개조해서 공부하는것을 좋아한다.

 

하지만 이 글의 경우에는 유달리 잘못된 코드가 있는지 모르겠어서 사실 글을 올리는 금일보다 2일 전에 밤에 화가 많이 났었다.(코린이의 분노... ㅋㅋ....)

 

어찌되었든, deprecated된 createStore를 사용하지 말고 이제는 reduxjs/toolkit를 이용해서 사용하도록 하자!