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 구문 필요 없음
- 복사해서 새로운 state를 만들어 return 해줄 필요 없이(구조 분해 할당으로 기존 state 할당해줄 필요 없이), mutate 하게
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를 통해서 가져다 지정할 수 있음
- actionCreator를
- 해당 방식을 사용하면, 기존에 사용하던 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;