redux toolkit

노영완·2023년 2월 14일
0

Redux

목록 보기
7/7
post-custom-banner

redux-toolkit

redux를 아무 라이브러리 없이 사용할 때 (action Type => 액션 함수 => 리듀서) 이렇게 1개의 액션을 생성합니다. 불변성을 지켜야하는 원칙 때문에 immer를 사용하고 devtools도 사용하고 store 값을 효율적으로 핸들링하여 불필요한 리렌더링을 막기 위해 reselect를 사용하며 비동기를 수월하게 하기 위해 thunk saga를 설치하여 middleware로 쓰입니다. 이렇게 총 4~5개 정도에 라이브러리를 설치하여 redux를 쓰게 됩니다.
하지만,
redux-toolkit은 redux에서 만든 공식적인 라이브러리로 saga를 제외한 위 기능 모두 지원합니다.
주의! redux-toolkit은 data가 아닌 payload로 받는다

npm i redux
npm i react-redux
npm i @reduxjs/toolkit

createAction(), createReducer()

createAction()

const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})

상수선언 createAction("type")
상수({payload}) createAction()을 통해서 action을 생성하지 않아도 된다.

createReducer(defaultState, reducerMap)

원래의 Reducer 사용방식은 기존의 state를 복사해서 새로운 state를 대치 형태
createReducer를 사용하면 push()로 state의 변경되는 요소만 변경 시킬 수 있음 물론, 새로운 state Object를 return 시켜도 상관없음
defaultState: 앱 처음 실행시, store의 state에 기본적으로 세팅된 state 구조 및 값
reducerMap: 객체 형태로 여러 reducer를 묶어 사용함
push, return 방식 둘다 지원함. 기존에 사용하던 redux-thunk 함수를 그대로 사용할 수 있다.

import {createAction} from '@reduxjs/toolkit'
const getUsersStart = createAction('GET_USERS_START');
const getUsersSuccess = createAction('GET_USERS_SUCCESS');
const getUsersFail = createAction('GET_USERS_FAIL');
// Reducer (Before using createReducer of Redux-Toolkit)
const initialState = {
    loading: false,
    data: [],
    error: null,
};
const reducer = (state = initialState, action) => {
    switch (action.type) {
        case getUsersStart.type:
            return { ...state, loading: true };
        case getUsersSuccess.type:
            return { ...state, loading: false, data: action.payload };
        case getUsersFail.type:
            return { ...state, loading: false, error: action.payload };
        default:
            return state;
    }
};
export default reducer;
// ------------------- After ------------------------------------
// Reducer (After using createReducer of Redux-Toolkit)
import {createReducer} from '@reduxjs/toolkit'
const initialState = {
    loading: false,
    data: [],
    error: null,
};
const reducer = createReducer(initialState, {
  // push() 방식
    [getUsersStart]: (state) => {
        state.push({ loading: true });
    },
  // return 방식
    [getUsersSuccess]: (state, action) => ({...state, loading: false, data: action.payload });
    ,
    [getUsersFail]: (state, action) => {
        state.push({ loading: false, error: action.payload });
    },
});
export default reducer;

configureStore() createSlice() createAsyncThunk()

기존의 redux 방식보다 action 함수 생성도 줄어들고 immer를 설치안해도 되고 코드의 양이 많이 줄어 가독성을 높일 수 있었지만 그래도 먼가 부족한 느낌이다. 어찌되었든 action을 불러와야하고 redux-thunk 라이브러리를 사용해야한다. 그래서 configureStore() createSlice() createAsyncThunk()를 사용해 실제 로그인/회원가입을 해보왔다.

configureStore()

const store = createStore(reducer, composeWithDevTools(applyMiddleware(thunk)));
const store = configureStore({
  reducer: rootReducer,
});

기존의 createStore를 쓰게되면 devtool, thunk를 연결해야하는데 configureStore를 쓰게되면 자동으로 devtool, thunk를 연결해준다.

configureStore({
  reducer: reducer를 연결할 수 있다.
  middleware: 우리가 개발에 필요한 middleware를 따로 연결할 수 있다. 
  devtool: devtool에 대한 핸들링이 가능하다.
  )}
// 내가 작성한 configureStore
const store = configureStore({
  reducer: reducer,
  middleware: (getDefalutMiddleware) => getDefalutMiddleware().concat(logger),
  devTools: process.env.NODE_ENV !== "production",
});

logger를 추가로 쓸 생각으로 바로 logger를 쓰면 기존의 configureStore에서 지니고 있는 middleware에 기능을 잃어버린다. 그래서 getDefalutMiddleware를 써 기존 middleware에 기능을 유지한채 logger를 기입하였다.
devtools에는 개발환경에서만 보이게끔 하기 위해서 저 코드를 기입하였다.

createSlice()

createSlice는 하나의 slice 객체를 인자로 받는다.
slice 객체는 {name, initialState, reducers, extraReducers}로 구성되어 있다.
name:string을 넣어서 prefix로 사용.
initialState: defaultState
reducers: 주로 동기처리 내부 함수에 쓰인다, reducer들을 만들면 자동으로 slice.action에 reducers에서 만든 reducer에 actionCreator 함수가 들어 있어 aciton 함수를 만들지 않고 action할 수 있다.
extraReducers: 주로 비동기처리 외부 함수에 쓰인다, 외부에서 만들어진 action을 통해 현재 slice에서 사용하는 initialState에 변경을 가하는 경우 처리받는 reducer이다.

import { combineReducers } from "redux";
import { logInSlice } from "./UserReducer";
export const reducer = combineReducers({
  login: logInSlice.reducer,
});

slice에 reducer로 가져와야 reducer가 작동한다.

// reducer => userReducer
import { createSlice } from "@reduxjs/toolkit";
import { logIn, signUp } from "../actions/userAction";
const initialState = {
  data: null,
};
export const logInSlice = createSlice({
  name: "loginSlice",
  initialState,
  reducers: {
    logOut(state, action) {
      state.data = null;
    },
  },
  extraReducers: {
    [logIn.pending]: (state, action) => {},
    [logIn.fulfilled]: (state, action) => {
      state.data = action.payload;
    },
    [logIn.rejected]: (state, action) => {
      state.data = action.error;
    },
    [signUp.pending]: (state, action) => {},
    [signUp.fulfilled]: (state, action) => {
      state.data = action.payload;
    },
    [signUp.rejected]: (state, action) => {
      state.data = action.error;
    },
  },
});

immer를 사용하지 않고 불변성유지 가능하며 reducers에 logOut 액션은 따로 정의 하지않고 사용할 수 있다. 그리고 extraReducers에 직접 작성한 비동기 함수에는 pending,fulfilled,rejected의 type을 가진 action을 자동으로 구현해준다. 이 3가지 타입에 맞게 reducer를 구현해 준다.

// build 방식
import { createSlice } from "@reduxjs/toolkit";
import { logIn, signUp } from "../actions/userAction";
const initialState = {
  data: null,
};
export const logInSlice = createSlice({
  name: "loginSlice",
  initialState,
  reducers: {
    logOut(state, action) {
      state.data = null;
    },
  },
  extraReducers: (builder) =>
    builder
      .addCase(logIn.pending, (state, action) => {})
      .addCase(logIn.fulfilled, (state, action) => {
        state.data = action.payload;
      })
      .addCase(logIn.rejected, (state, action) => {
        state.data = action.error;
      })
      .addCase(signUp.pending, (state, action) => {})
      .addCase(signUp.fulfilled, (state, action) => {
        state.data = action.payload;
      })
      .addCase(signUp.rejected, (state, action) => {
        state.data = action.error;
      }),
});

(builder) => builder .addCase(비동기함수.(pending,fulfiiled,rejcted)를 이용해 createSlice를 사용할 수 있다 차 후 typescript를 쓸 때 타입지정에 용이하게 쓸 수 있어 바꾸어 주었다.

createAsyncThunk()

slice로 구현한 state를 변경하는 reducer는, 기존의 react-thunk 사용방식으로는 안됨.
toolkit에서 제공하는 createAsyncThunk()를 활용해서 비동기 작업을 구현할 수 있음.

import { createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
export const logIn = createAsyncThunk("login", async (params, thunkAPI) => {
  try {
    const response = await axios.post(
      "http://localhost:8080/users/login",
      params
    );
    return response.data;
  } catch (e) {
    return thunkAPI.rejectWithValue(e);
  }
});
export const signUp = createAsyncThunk("SIGNUP", async (params, thunkAPI) => {
  try {
    const response = await axios.post(
      "http://localhost:8080/users/create",
      params
    );
    return response.data;
  } catch (e) {
    return thunkAPI.rejectWithValue(e);
  }
});

createAsyncThunk(type, payloadCreator, options)로 구현.
type: sting 형식 해당 요청의 type명
payloadCreator: actionCreator로 payload와 함께 보내져 요청되는 비동기 함수 실행 부분 (인자 두개를 받음)
arg: 첫번째 파라미터로 지정하면, actionCreator를 사용하면서 보낼 payload(인자)를 받아 실행하고자 하는 비동기 함수를 구성하는데 사용될 사용자 입력으로 활용
thunkAPI: dispatch, getState, rejectWithValue, fulfillWithValue 등의 함수를 실행 할수 있는 API 묶음
기본적으로 해당 함수의 return 값은 fulfilled로 처리하여 payload로 보내지고, error는 thukAPI,rejectWithValue(error)를 통해서 받아 action.error로 보내짐

로그인/회원가입

// App.js
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { logIn, signUp } from "./actions/userAction";
import { logInSlice } from "./reducers/UserReducer";
function App() {
  const [email, setEmail] = useState("");
  const [passWord, setPassWord] = useState("");
  const dispatch = useDispatch();
  const user = useSelector((state) => state.login);
  let body = { email: email, password: passWord };
  const onClickSignUP = () => {
    dispatch(signUp(body)).then(
      (res) => console.log(res),
      alert("가입이 정상적으로 완료되었습니다")
    );
  };
  const logOut = () => {
    dispatch(logInSlice.actions.logOut());
  };
  const onClickSignIn = () => {
    dispatch(logIn(body));
  };
  const onChangeEmail = (e) => {
    const { value } = e.target;
    setEmail(value);
    console.log(email);
  };
  const onChangePassWord = (e) => {
    const { value } = e.target;
    setPassWord(value);
    console.log(passWord);
  };
  return (
    <>
      <input onChange={onChangeEmail} value={email} />
      <input onChange={onChangePassWord} value={passWord} />
      {!user.data ? (
        <button onClick={onClickSignUP}>회원가입</button>
      ) : (
        <>
          <button onClick={onClickSignIn}>로그인</button>
          <button onClick={logOut}>로그아웃</button>
        </>
      )}
      {user.data && <div>{user.data.message}</div>}
    </>
  );
}
export default App;

특징으로 loginslice를 불러와 action에 logOut을 해줌으로써 따로 액션을 생성하지 않고 logOut reducer를 불러왔다. 비동기 action 즉, extraredcers는 액션을 dispatch해 불러왔다.

틀린내용이 있으면 댓글 남겨주시면 감사하겠습니다.

post-custom-banner

0개의 댓글