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는 필수는 아니고,
prefix
와namespace
를 설정할 수 있음- 객체 형태(
{}
)로 넣어서 값을 지정해주면 됨 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
를 사용해서 복사를 시켜줘야 함)
- 무조건 actionMap의 형태
- defaultState
- defaultState의 경우에는 단지, 처음에 앱이 실행되었을 때 reducer를 통해서 storeState의 상태를 성절해주는 역할 뿐인것 같다.
- options
- 옵션은 createActions와 마찬가지의 기능하고, createActions에서 만들어지면서 옵션을 가진 action들을 reducer가 처리할 때 이러한 옵션을 해독하기 위한
prefix
,namespace
를 설정해준다고 볼수 있겠다.
- 옵션은 createActions와 마찬가지의 기능하고, createActions에서 만들어지면서 옵션을 가진 action들을 reducer가 처리할 때 이러한 옵션을 해독하기 위한
// 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를 구현해야 함
- 기존의 thunk와 다른점은 promise middleware 처럼 비동기 작업을 수행하는 action을 만들면 알아서, 해당 action을 참고해서
- slice에 붙은 Reducers의 경우에는 비동기 작업하는 함수는 딱히 필요 없고, state 값을 변경하는 경우에 사용하는데, actionCreator 함수명으로 작성하면 되고, reducer 작업처리는
state.push()
에 바뀔 데이터만 넣어주면 해당 데이터만 변경되고 나머지는 그대로라서...state
처리를 할 필요가 없다.- 하지만, return 형식으로 사용하게 된다면 원래 쓰던대로 사용하고
...state
처리를 해줘야 한다. state.push()
의 경우에는 return 처리가 아니라는 점을 주의해야 한다.
- 하지만, 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);
});
}
- 다음시간에는 구체적인 사용방법과 특징을 작성할 예정