[React]Redux-toolkit 사용법

Yoon Ki Hyuk·2022년 5월 9일
7

React

목록 보기
5/13
post-thumbnail

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 함수,할당 방식

1 : 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;

2 : 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()를 활용해서 비동기 작업을 구현할 수 있음

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;

출처: https://goforit.tistory.com/159

profile
개발은 낭만이다

0개의 댓글