본문 바로가기

Redux

20210702 Redux08 : redux-toolkit 사용법, createAction, createReducer, createSlice(reducers, extraReducers 작성법들), configureStore, createAsyncThunk

Redux 08





redux-tookit 사용법


createAction, createReducer 방식


  • store middleware등의 환경을 건들지 안고 바로 적용할 수 있는 방식
  • 하지만, 크게 코드가 줄어 들진 않음

createAction()


  • redux-actions에서 제공하고 있던 createAction()과 같은 방식으로 작동한다.
  • 기존의 Action creator를 세부적으로 만들 필요 없이 actionCreator를 통해서 type, payload 형태의 Actoin을 만들어줌
import { createAction } from "@reduxjs/toolkit";

const getUsersStart = createAction("GET_USERS_START");
const getUsersSuccess = createAction("GET_USERS_SUCCESS");
const getUsersFail = createAction("GET_USERS_FAIL");

console.log(getUsersStart, getUsersSuccess, getUsersFail);
// 단지 변수 명으로 사용시 -> function return

console.log(getUsersStart.type, getUsersSuccess.type, getUsersFail.type);
/* Type을 붙여서 사용시 -> Type return
GET_USERS_START GET_USERS_SUCCESS GET_USERS_FAIL */

console.log(getUsersStart(), getUsersSuccess(), getUsersFail());
/* 함수 호출로 사용시 -> action return (액션의 경우 어쨌거나, payload와 함깨 보내짐)
{ type: 'GET_USERS_START', payload: undefined } 
{ type: 'GET_USERS_SUCCESS', payload: undefined } 
{ type: 'GET_USERS_FAIL', payload: undefined } */


createReducer()


  • 원래의 Reducer 사용방식은 기존의 state를 복사해서 새로운 state를 만들어서 기존 것을 넣고 변경된 내용을 덮어 쓰는 형태 였음

  • createReducer를 사용하면,
    • 복사해서 새로운 state를 만들어 return 해줄 필요 없이(구조 분해 할당으로 기존 state 할당해줄 필요 없이), mutate 하게 push()로 state의 변경되는 요소만 변경 시킬 수 있음 (push는 어떤 것도 return 하지 않음)
    • 물론, 새로운 state Object를 return 시켜도 상관 없음
    • switch 구문 필요 없음

  • createReducer(defaultState, reducerMap)
    • defaultState : 앱 처음 실행시, store의 state에 기본적으로 세팅된 state 구조 및 값
    • reducerMap: {} 객체 형태로 여러 reducer를 묶어 사용함
      • actionCreator를 []로 감싸서 "computed property syntax" 를 사용함 (MDN :computed property syntax)
        • computed property syntax는 property의 이름을 밖의 변수를 참조시커나, 연산 할수 있게 하여 동적인 property 명을 가질 수 있게 함
      • push 방식, return 방식 둘다 지원함
        • push : 전개 연산자를 사용할 필요가 없음, 어떤 것도 return 하지 않음
        • return: 전개연산자를 사용해 주어야 함, return 형식으로 해줘야 함
      • action에서 전달하는 데이터는 payload를 통해서 가져다 지정할 수 있음
  • 해당 방식을 사용하면, 기존에 사용하던 redux-thunk 함수를 그대로 호환해서 사용할 수 있음

// actionCreator
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;

// Thunk ActionCreator - createReducer 방식과 그대로 호환 됨
export function getUsersThunk() {
    return async (dispatch, getState) => {
        try {
            dispatch(getUsersStart());
            const res = await axios.get('https://api.github.com/users');
            dispatch(getUsersSuccess(res.data));
        } catch (error) {
            dispatch(getUsersFail(error));
        }
    };
}



Slice


configureStore()


  • 기존의 createStore를 대체해서 configureStore() 사용하면, Thunk, dev tool까지 자동으로 연결해줌
  • 주의 할 점은, configureStore에 넣을 인자인 rootReducer 형태를 객체로 {reducer: rootReducer}로 만들어 주어서 넣어 주어야 함 그리고 property명은 reducer로 무조건 해줘야 함
import rootReducer from "./modules/reducer";
import { configureStore } from "@reduxjs/toolkit";

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

export default store;

  • rootReducer를 reducer이름으로 사용하면 property와 이름이 같아져서 객체 단축 표현으로 {reducer}로 사용할 수 있음
import reducer from "./modules/reducer";
import { configureStore } from "@reduxjs/toolkit";

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

export default store;
  • rootReducer를 연결할 때는 대신에 해당 slice의 reducer로 출력해서 연결해줘야 함
    • slice의 경우 reducer와 action을 모두 포함하고 있기 때문에 정확하게 reducer를 연결해 줄 필요가 있음
import { combineReducers } from "redux";
import users from "./users1";

const reducer = combineReducers({
  users: users.reducer,
});
// 애초에 slice를 users.reducer로 export default하면 그냥 reducer 모양으로 받아서 넣을 수 있음

export default reducer;



createSlice()

  • createSlice는 하나의 slice 객체를 인자로 받음
  • slice 객체는 {name, initialState, reducers, extraReducers}로 구성되어 있음
    • name: string을 넣어서 prefix로 사용
    • initialState: defaultState가 들어감, (변수를 initialState로 지정하면, 단축으로 작성할 수 있음)
    • reducers: slice 안에서 사용할 reducer 들을 만들수 있음, 해당 reducer들을 만들면 자동으로 slice.action에 reducers에서 만든 reducer에 대한 actionCreator 함수가 들어 있음
    • extraReducers: slice에서 만들어진 reducers에 의한 action, reducer가 아닌 외부에서 만들어진 action을 통해 현재 slice에서 사용하는 initialState에 변경을 가하는 경우 처리받는 reducer임 (비동기 작업 함수 처리 등에 사용됨)

reducers 작성법 2가지


  • reducer를 작성시 처리하는 방식은 2가지가 있다.
    • 방법1(함수, 직접 할당 방식): 직접 값을 변경하는 방식으로 기존 값에 변경을 주는 함수를 사용하거나, 할당을 이용
      • js에서 사용하는 기존값에 변경을 가하는 Array 함수 등인 push 등이 있겠다.
      • 초기 자료구조가 어떻게 되어 있는지에 따라 변수에 사용할수 있는 함수는 달라지겠다.
      • 주의할점은, 집어 넣는 값과 기존의 자료구조가 어떻게 되어 있는지 조심해야한다.
        • payload 자체로 보내기 때문에 reducer에서 값을 어떻게 받게 할 것인지 조심해야 함
    • 방법2(복사 return 방식): 기존에 사용하던 방식으로, return을 주어 기존의 state는 복사하여 가져오고 변경된 값만 덮어 씌우는 형식으로 지정
      • 일단, state 전체를 바꾸는 거라서 오히려 전체구조를 그리면서 할수 있어서 어떻게 들어가는지 함수 고려를 하지 않아도 됨
      • 하지만, ...state를 사용해서
// defaultState
const initialState = {
  loading: false,
  data: [],
  error: null,
};

// slice reducers 방법01 : 있던 값을 바꾸는 형식 (state에 직접적으로 변경을 가함, 함수방식)
// 직접 변경을 가하기 때문에 기존값을 풀어써주는 전개 연산자가 필요가 없음
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {
    getUsersStart: (state, action) => {
      state.loading = true;
    },
    getUsersSuccess: (state, action) => {
      state.loading = false;
      state.data = action.payload;
    },
    getUsersRemove: (state, action) => {
      state.loading = false;
      state.data = [];
    },
  },
  extraReducers: {},
});
export const { getUsersRemove, getUsersSuccess, getUsersStart } = users.actions;

// slice reducers 방법02 : 기존 state를 복사하여 새로운 값을 만들어서 state에 세팅하는 방식(return 방식)
// 기존 값을 가져와 반영해야 함으로 전개연산자 필요
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {
    getUsersStart: (state, action) => ({ ...state, loading: true }),
    getUsersSuccess: (state, action) => ({
      ...state,
      loading: false,
      data: action.payload,
    }),
    getUsersRemove: (state, action) => ({
      ...state,
      loading: false,
      data: [],
    }),
  },
  extraReducers: {},
});
export const { getUsersRemove, getUsersSuccess, getUsersStart } = users.actions;

extraReducer 작성법 2가지


  • 자신이 구현한 외부 비동기 작업 함수 사용시 사용되며, 외부에서 만들어진 action을 처리하기 때문에 외부에서 만들어진 함수를 property이름으로 사용하고, 자동으로 pending, fulfilled, rejected의 type을 가지 action을 구현해줌
  • 그래서 pending fulfilled, rejected 에 맞게 extraReducers를 작성하면됨

  • map Object notation 방식 - return 방식 or 함수,할당 방식
  • builder callback notation 방식 - return 방식 or 함수,할당 방식

01 : map Object notation 방식 - return 방식 or 함수,할당 방식


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

// Slice extraReducers
// 방식1: map Object notation
// 1. return 방식 (전개 연산자 필요)
const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {},
  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.error,
    }),
  },
});

export default users;


// 방식1: map Object notation
// 2. 함수 및 할당 방식 (전개 연산자 불필요)
const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: {
        [getUsersThunk.pending]: (state, action) => {
            state.loading = true;
        },
        [getUsersThunk.fulfilled]: (state, action) => {
            state.loading = false;
            state.data = action.payload;
        },
        [getUsersThunk.rejected]: (state, action) => {
            state.loading = false;
            state.error = action.error;
        },
    },
});

export default users;


02 : builder callback notation 방식 - return 방식 or 함수,할당 방식


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

// 방식2: builder callback notation
// 1. return 방식 (전개 연산자 필요함)
const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(getUsersThunk.pending, (state, action) => ({
                ...state,
                loading: true,
            }))
            .addCase(getUsersThunk.fulfilled, (state, action) => ({
                ...state,
                loading: false,
                data: action.payload,
            }))
            .addCase(getUsersThunk.rejected, (state, action) => ({
                ...state,
                loading: false,
                error: action.error,
            }));
    },
});

export default users;

// 방식2: builder callback notation
// 2. 함수 및 할당 방식 (전개 연산자 불필요)

const users = createSlice({
    name: 'usersReducer',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(getUsersThunk.pending, (state, action) => {
                state.loading = true;
            })
            .addCase(getUsersThunk.fulfilled, (state, action) => {
                state.loading = false;
                state.data = action.payload;
            })
            .addCase(getUsersThunk.rejected, (state, action) => {
                state.loading = false;
                state.error = action.error;
            });
    },
});

export default users;



createAsyncThunk(): 비동기 작업 함수 작성


  • slice로 구현한 state를 변경하려면, 기존의 react-thunk 사용방식으로는 안됨
  • toolkit에서 제공하는 createAsyncThunk()를 활용해서 비동기 작업을 구현할 수 있음
  • redux-toolkit 공식 문서: createAsyncThunk
  • createAsyncThunk(type, payloadCreator, options)
    • type: 해당 요청의 type명으로, prefix를 포함해서 작성해 주어야 한다. (pending, fulfilled, rejected는 알아서 상황에 맞게 붙여짐으로 고려하지 않아도 됨)
    • payloadCreator: actionCreator로 payload와 함께 보내져 요청되는 비동기 함수 실행 부분 (인자 두개를 받음)
      • arg: 첫번째 파라미터로 지정하면, actionCreator를 사용하면서 보낼 payload(인자)를 받아 실행하고자 하는 비동기 함수를 구성하는데 사용될 사용자 입력으로 활용할 수 있음
      • thunkAPI: dispatch, getState, rejectWithValue, fulfillWithValue 등의 함수를 실행 할수 있는 API 묶음
  • 기본적으로 해당 함수의 return 값은 fulfilled로 처리하여 payload로 보내지고, error는 thukAPI,rejectWithValue(error)를 통해서 받아 action.error로 보내짐
// createAsyncThunk : Thunk 비동기 작업
export const getUsersThunk = createAsyncThunk(
  "users/getUsersThunk",
  async (thunkAPI) => {
    try {
      const res = await axios.get("https://api.github.com/users");
      return res.data;
    } catch (error) {
      return thunkAPI.rejectWithValue(error);
    }
  }
);

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

const users = createSlice({
  name: "usersReducer",
  initialState,
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(getUsersThunk.pending, (state, action) => {
        state.loading = true;
      })
      .addCase(getUsersThunk.fulfilled, (state, action) => {
        state.loading = false;
        state.data = action.payload;
      })
      .addCase(getUsersThunk.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error; // action.error인 것을 주의
      });
  },
});

export default users;