본문 바로가기

Redux

20210701 Redux07 : vanilla redux vs redux-actions vs redux-toolkit

Redux 07





redux-actions 활용과 느낌


기존 환경


  • redux-actions를 사용하는 경우, 기본적으로 다른 여러가지 패키지들과 사용하게 된다.
    • redux-actions : 리덕스 액션스는 덕스 패턴을 만들때 편하게 작성시켜 코드를 좀더 깔끔하게 해준다.
    • react-redux : redux와 react를 연결할 때 쓰는 Provider, 그리고 react에서 state의 값을 가져오거나, 변경하고자 할때 요청을 useSeletor, useDispatch를 통해서 쉽게 사용할 수 있게함
    • redux-thunk : 비동기 작업을 Action으로 수행하고, 해당 작업을 통해 가져온 데이터 같은 것을 redux에 반영하게 할 수 있음
    • redux-devtools-extension : 리덕스 작업시 브라우저에서 어떻게 처리되는지 볼수 있게 추적해주고, 브라우저에서도 개발 tool이 아주 잘 되어 있음



redux-actions 사용 전


  • redux-thunk, react-redux, redux-devtools-extension을 사용하는 것은 똑같음
  • 그러나, Type 작성, actionCreator 작성, defaultState작성, Reducer 작성 등의 한개의 모듈안에 엄청난 코드가 들어가게 된다.
    • 단지 외부에서 데이터를 요청해서 가져오는 비동기 작업 1번 했을 뿐인데, start - success - fail 이렇게 3가지 경우의 수로 나누어 state 변경(dispatch) 처리를 해줘야 한다.
  • 그렇기 때문에, redux를 쓰면서 너무 긴 코드와 반복, 중복이 난무하는데 어떻게 해야 하나 생각했다.
    • 기존에 만들어 놓은 instagram clone 프로젝트를 redux화 시킬려고 했는데 워낙 쓰이는 변수도 많은데, 이걸 구현하려면 이렇게 코드양이 많은 구조의 redux를 사용해야 한다고 생각니 엄두가 나지도 않았다.
    • 그래서 redux-actions에 발을 들여 어떤지 사용해 보고, 숙달 시키려고 노력했다.
import axios from "axios";

// Action Types
// thunk
const GET_USERS_START = "redux-start/users/GET_USERS_START";
const GET_USERS_SUCCESS = "redux-start/users/GET_USERS_SUCCESS";
const GET_USERS_FAIL = "redux-start/users/GET_USERS_FAIL";

// ActionsCreator
export function getUsersStart() {
  return {
    type: GET_USERS_START,
  };
}

export function getUsersSuccess(data) {
  return {
    type: GET_USERS_SUCCESS,
    data,
  };
}

export function getUsersFail(error) {
  return {
    type: GET_USERS_FAIL,
    error,
  };
}

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

// defaultState
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// Reducer
export default function reducer(state = initialState, action) {
  // thunk
  if (action.type === GET_USERS_START) {
    return {
      ...state,
      loading: true,
      error: null,
    };
  }

  if (action.type === GET_USERS_SUCCESS) {
    return {
      ...state,
      loading: false,
      data: action.data,
    };
  }

  if (action.type === GET_USERS_FAIL) {
    return {
      ...state,
      loading: false,
      error: action.error,
    };
  }

  return state;
}

// Thunk ActionCreator : 비동기 작업
export function getUsersThunk() {
  return async (dispatch, getState, { history }) => {
    try {
      console.log(history);
      dispatch(getUsersStart());
      // API가 빨리 끝나서 분석을 위해서 sleep을 줌
      await sleep(1000);
      const res = await axios.get("https://api.github.com/users");
      dispatch(getUsersSuccess(res.data));
      history.push("/");
    } catch (error) {
      dispatch(getUsersFail(error));
    }
  };
}



redux-actions 사용 후


  • 활용해서 작성해 보았지만, prefix를 작성할 때 편한 것 말고는 딱히 크게 코드양을 줄어드는 것도 아닌것 같았다.
  • 어쨌거나, actionCreator를 작성해야 했다.
    • 이때 actionsCreator와 reducer가 중복되는 곳이 많은데 이렇게 밖에 못줄이나 생각이 들어 아쉽긴 했다.
  • 대신, 기존에 사용하던 환경에서 action, reducer 작성 방식만 달라져서 reducerCombine 이나, middleware 설정은 건드리지 않아서 좋았다.
import axios from "axios";
import { createActions, handleActions } from "redux-actions";

// ActionsCreator (redux-actions)
const { getUsersStart, getUsersSuccess, getUsersFail } = createActions(
  {
    GET_USERS_SUCCESS: (data) => data,
    GET_USERS_FAIL: (error) => error,
  },
  "GET_USERS_START",
  {
    prefix: "redux-start/users",
  }
);

// defaultState
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// handleActions
// handleActions(reducerMap, defaultState[, options])
const reducer = handleActions(
  {
    GET_USERS_START: (state) => ({
      ...state,
      loading: true,
    }),
    GET_USERS_SUCCESS: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    GET_USERS_FAIL: (state, action) => ({
      ...state,
      loading: false,
      error: action.payload,
    }),
  },
  initialState,
  { prefix: "redux-start/users" }
);

export default reducer;

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

// Thunk ActionCreator : 비동기 작업
export function getUsersThunk() {
  return async (dispatch, getState) => {
    try {
      dispatch(getUsersStart());
      await sleep(1000);
      const res = await axios.get("https://api.github.com/users");
      dispatch(getUsersSuccess(res.data));
      // history.push('/');
    } catch (error) {
      dispatch(getUsersFail(error));
    }
  };
}



redux-actions 사용법


createActions()


  • createActions(actionMap, ...identityActions[, options])

    • creatActions는 actionMap, identityActions에서 활용되는 대문자 스네이크 케이스(UPPER_SNAKE_CASE)를 알아서, Type으로 사용하고 이를 camelCase 형식으로 변경해서 creator 함수 명으로 사용됨

    • createActions에 넣는 인자는 총 3가지임

      • 근데, actionMap, identityActions 중에 하나만 단독으로 사용하게 되어도 매개변수 위치 상관 없이, 알아서 인식함

      • actionMap 형식의 경우는 action들을 넣은 객체({}) 형태의 묶음으로 state 변환 요청으로 dispatch시 바꿀 데이터를 주는 경우 reducer에서 payload로 들어오는 것을 제어 할 수 있음
        • getUsersSuccess() creator를 사용해서 getUserSuccess(data) 처럼 데이터를 주는 경우
        • 받는 인자를 어떻게 가공해서 payload가 어떤 형태의 자료를 보낼지 결정
        • GET_USERS_SUCCESS: (data, index, number) => {data: data, index: index, number: number} 의 형식인 경우 payload는 {data: data, index: index, number: number}를 전달함으로 써,
        • reducer에서는 {dataState: action.payload.data, indexState: action.payload.index, ...state} 이런 형식으로 dispatch 받은 값을 storeState에 반영할 수 있음

      • identityActions의 경우에는 딱히 payload가 없거나, 가공해야 할 필요가 없는 경우 ""를 활용한 string 형식으로 넣으면 알아서 해당 액션 함수를 만들어 줌

      • 마지막에 붙는 options는 필수는 아니고, prefixnamespace를 설정할 수 있음
        • 객체 형태({})로 넣어서 값을 지정해주면 됨
        • prefix : ducks 패턴에 맞게 type을 변경해주기 위한 prefix를 설정 할 수 있음
        • namespace: ducks패턴 type 명에서 쓰이는 / (Seprator)를 변경 할 수 있음
// ActionsCreator (redux-actions)
export const { getUsersStart, getUsersSuccess, getUsersFail } = createActions(
  {
    GET_USERS_SUCCESS: (data) => data,
    GET_USERS_FAIL: (error) => error,
  },
  "GET_USERS_START",
  {
    prefix: "redux-start/users",
  }
);


handleActions

  • handleActions는 해당 Action들을 처리하는 reducer의 역할을 함
  • handleActions(actionMap[, defaultState], options)
    • handleActions의 경우에는 createActions와 다르게, identityActions의 형태로 다룰수는 없다.
    • actionMap
      • 무조건 actionMap의 형태 {} 객체로 묶어 넣어 주어야 한다.
      • 똑같이 UPPER_SNAKE_CASE 형태로 작성하게 됨
      • state, action을 인자로 받아서 return 처리하여 활용함
      • 주의할 점은, 각 reducer가 값을 처리할때 ...state처럼 기존의 state를 복사하고 해당 값에 변화하는 값을 덮어 씌우는 형태로 return 처리를 해줘야 한다는 것이다.
      • defaultState를 설정했다고 해서 reducer에서 값 변환시 자동으로 state의 변화하지 않는 값을 유지하게 해주진 않는다.(그래서 ...state를 사용해서 복사를 시켜줘야 함)
    • defaultState
      • defaultState의 경우에는 단지, 처음에 앱이 실행되었을 때 reducer를 통해서 storeState의 상태를 성절해주는 역할 뿐인것 같다.
    • options
      • 옵션은 createActions와 마찬가지의 기능하고, createActions에서 만들어지면서 옵션을 가진 action들을 reducer가 처리할 때 이러한 옵션을 해독하기 위한 prefix, namespace를 설정해준다고 볼수 있겠다.
// defaultState
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// handleActions
// handleActions(reducerMap, defaultState[, options])
const reducer = handleActions(
  {
    GET_USERS_START: (state) => ({
      ...state,
      loading: true,
    }),
    GET_USERS_SUCCESS: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    GET_USERS_FAIL: (state, action) => ({
      ...state,
      loading: false,
      error: action.payload,
    }),
  },
  initialState,
  { prefix: "redux-start/users" }
);

export default reducer;

  • 비동기 작업의 경우에는 thunk를 사용하던 방식으로 만들면 된다.



redux-toolkit 활용과 느낌


redux-actions를 활용하면서, 실망감을 느끼고 redux-tookit을 사용해 보았다. 일단, redux-toolkit에서 제공하는 createAction과 createReducer를 사용해 보았고, 이둘의 사용보다는 redux-tookit의 slice 사용이 가장 맘에 들었다.


  • slice를 사용하면, reducer 작성 한번으로 action까지 다룰수 있게 되어 상당히 코드를 절약 할 수 있다. (중복제거를 통한 코드 축약)
  • thunk, redux-devtools-extension이 내장 되어있어서 따로 설치하지 않고도, 바로 사용이 가능하다. (환경의 용이성)
  • thunk를 활용해서 비동기 작업을 하고 싶은 경우에는 내장함수인 createAsyncThunk()를 활용해서 구현할 수 있다 (비동기 처리의 용이성)
    • 기존의 thunk와 다른점은 promise middleware 처럼 비동기 작업을 수행하는 action을 만들면 알아서, 해당 action을 참고해서 pending, fulfilled, rejected의 type을 가진 action을 만들어 주기 때문에 편함
    • slice에서 extraReducers에 넣어 활용하면 slice 기존 Reducers에 연결된 state의 값을 변화를 줄 수 있다.
      • 주의) 비동기 작업의 reducer를 slice Reducers에 구현하면, 작동하지 않음
      • 비동기 작업의 경우 애초에 slice 외부에서 만들어 지기 때문에 extraReducers를 통해 reducer를 구현해야 함

  • slice에 붙은 Reducers의 경우에는 비동기 작업하는 함수는 딱히 필요 없고, state 값을 변경하는 경우에 사용하는데, actionCreator 함수명으로 작성하면 되고, reducer 작업처리는 state.push()에 바뀔 데이터만 넣어주면 해당 데이터만 변경되고 나머지는 그대로라서 ...state 처리를 할 필요가 없다.
    • 하지만, return 형식으로 사용하게 된다면 원래 쓰던대로 사용하고 ...state 처리를 해줘야 한다.
    • state.push()의 경우에는 return 처리가 아니라는 점을 주의해야 한다.

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

// defaultState
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// 비동기 작업
export const getUsersThunk = createAsyncThunk(
  "users/getUsersThunk",
  async (thunkAPI) => {
    try {
      await sleep(1000);
      const res = await axios.get("https://api.github.com/users");
      return res.data;
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

// Slice
const users = createSlice({
  name: "redux-test/users",
  initialState,
  reducers: {
    /*
    // state.push 함수로 처리하는 경우 -> getUsersStart
     getUsersStart: (state, action) => {
      state.push({ loading: true });
    },
    // return으로 처리하는 경우 -> getUsersSuccess
    getUsersSuccess: (state, action) => ({
      loading: false,
      data: action.payload,
    }),
    getUsersFail: (state, action) => {
      state.push({ loading: false, error: action.payload });
    }, */
  },
  extraReducers: {
    [getUsersThunk.pending]: (state, action) => ({
      ...state,
      loading: true,
    }),
    [getUsersThunk.fulfilled]: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    [getUsersThunk.rejected]: (state, action) => ({
      ...state,
      loading: false,
      error: action.payload,
    }),
  },
});

export default users;

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}
  • 다음시간에는 구체적인 사용방법과 특징을 작성할 예정