
오늘은 리덕스 툴킷에 대해 알아보자~!
Redux Toolkit은 Redux를 더 사용하기 쉽게 만들기 위해 Redux에서 공식 제공하는 개발도구이다. Redux Toolkit은 아래와 같은 Redux의 문제점을 보완하기 위해 등장하였다.
리덕스를 라이브러리 없이 사용 시 1개의 액션을 생성해도
액션타입 정의 -> 액션함수 생성 -> 리듀서 정의의 작업이 필요하다. 많아지는 액션을 관리하기 위해redux-actions을, 불변성 보존을 위한immer, store값을 효율적으로 핸들링하여 불필요한 리렌더링을 막기 위해reselect, 비동기 작업을 위한thunk와saga등 리덕스의 유효한 기능을 사용하기 위해 4~5개의 라이브러리를 사용해야 했다.하지만Redux Toolkit은 내장된 기능으로saga를 제외한 위의 모든 기능을 제공한다.

(출처 : 리덕스 홈페이지)
- Simple: 스토어 설정, 리듀서 생성, 불변성 업데이트 로직 사용을 편리하게 하는 기능 포함
- Opitionated: 스토어 설정에 관한 기본 설정 제공, 일반적으로 사용되는 redux addon이 내장
- Powerful : Immer에 영감을 받아 '변경'로직으로 '불변성'로직 작성 가능, state 전체를
slice로 자동으로 만들 수 있음- Effective : 적은 코드에 많은 작업을 수행 가능
이를 위해 immer produce,reselect, ducks pattern, Redux Devtools, FSA 규약, typescript, 미들웨어(thunk 한정) 등을 지원하고 있다. 오늘은 configureStore,createAction, createReducer,createSlice,createAsyncThunk에 대해 알아보자
const INCREMENT = 'counter/increment' 📌 1
function increment(amount) { 📌 2
return {
type: INCREMENT,
payload: amount
}
}
const action = increment(3) ✅ 사용
// { type: 'counter/increment', payload: 3 }
action함수를 정의하기 위해 위와 같이 2번의 과정이 필요하며 별도의
액션타입을설정해줘야 한다.
const increment = createAction('counter/increment') 📌 1
let action = increment() ✅ 사용
// { type: 'counter/increment' }
action = increment(3) ✅ 사용
// returns { type: 'counter/increment', payload: 3 }
action을 사용하기 위해 한번의 과정이면 된다.
type만 인자로 넣어주면 default로 type을 가진action object(액션함수)를 생성해주며
만약 이 액셤함수를 호출 시 parameter를 추가로 넣어준다면 자동으로 payload의 value값으로 들어간다.
function counterReducer(state = 0, action) {
switch (action.type) {
case 'increment':
return state + action.payload
case 'decrement':
return state - action.payload
default:
return state
}
}
1)기존에는 reducer 사용시 switch 등의 조건문으로 action type을 구분해 특정 로직을 수행했다.
2) default를 항상 명시 해주어야 한다.
const counterReducer = createReducer(0, {
increment: (state, action) => state + action.payload,
decrement: (state, action) => state - action.payload
})
별도 조건문 필요 없이 첫 번째 인자는 initailState, 두 번째 인자는 caseReducers를 가진다.
const increment = createAction('increment')
const decrement = createAction('decrement')
const counterReducer = createReducer(0, {
[increment]: (state, action) => state + action.payload,
[decrement.type]: (state, action) => state - action.payload
})
createSlice는 Ducks 패턴을 사용해 action과 reducer를 전부 가진 함수이다.
createSlice({
name: 'reducerName',
initialState: [],
reducers: {
action1(state, payload) {
//action1 logic
},
action2(state, payload) {
//action2 logic
},
action3(state, payload) {
//action3 logic
}
}
})
name 속성은 액션의 경로를 잡아줄 이름이고 initialState은 초기 state를 나타낸다.
reducer는 이전의 리듀서 함수와 같이 액션에 따라 특정 로직을 수행하는 것은 같으나
기존에는 액션생성함수와 액션 타입을 선언해 사용했다면 createSlice는 액션을 선언하고 해당 액션이 dispatch 되면 바로 state을 가지고 해당 액션을 처리한다.
즉, reducers 안의 코드들은 Action Type, Action Create Function, Reducer 의 기능이 합쳐져 있는 셈이다.
import { createStore } from "redux";
import rootReducer from "./Redux/Reducer";
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
const store = createStore(rootReducer, devTools);
1)
store를 생성할 때 redux가 제공하는 createStore을 이용해 생성하며 redux가 제공하는combineReducers로 리듀서 함수를 묶어준root리듀서를 인자로 넣는다.
2) devTools 사용 시 두번째 인자로 넣어준다.
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todosReducer,
},
});
1) 별도의
combineReducers로 리듀서를 묶어줄 필요 없이 reducer 필드를 필수적으로 넣고 그안에 리듀서 함수를 넣으면 된다.
2) default로 redux devtool을 제공한다.
//액션타입
const increase = "INCREASE";
const decrease = "DECREASE";
const increaseFive = "INCREASE_FIVE";
//액션생성함수
export const increaseAction = () => {
return { type: increase };
};
export const decreaseAction = () => {
return { type: decrease };
};
export const increaseOtherAction = (number) => {
return { type: increaseFive, data: number };
};
//초기 상태값
const initialState = {
number: 30,
};
//리듀서
export const numberReducer = (state = initialState, action) => {
switch (action.type) {
case increase:
return {
...state,
number: state.number + 1,
};
case decrease:
return (state = {
...state,
number: state.number - 1,
});
case increaseFive:
return (state = {
...state,
number: state.number + action.data,
});
default:
return state;
}
};
name : 해당 모듈의 이름 작성
initialState : 해당 모듈의 초기값을 세팅
reducers : 리듀서를 작성하고 이때 해당 리듀서의 키값으로 액션함수가 자동으로 생성됨..
extraReducers : 액션함수가 자동으로 생성되지 않는 별도의 액션함수가 존재하는 리듀서를 정의 (선택 옵션)
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { AppThunk, RootState } from '../../app/store';
export const counterSlice = createSlice({
name: 'counter',
initialState: {
value: 0,
};
reducers: {
increment: state => {
state.value += 1;
},
decrement: state => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export default counterSlice.reducer;
기존 redux에서 비동기 처리를 할 경우 thunk, saga, redux-observable 등의 미들웨어를 사용하여 한 개의 비동기 액션에 대해 pending, success, failure의 상태를 생성하여 처리하였다. 이때 각 상태를 만드는 것을 유틸 패키지를 받거나 직접 구현해야 했다
리덕스 툴킷에서는 createAsyncThunk를 제공하여 Thunk의 미들웨어를 간편하게 사용할 수 있다
(단 다른 것은 이전 버전과 같이 직접 구현하거나 패키지를 받아야함)
또한 액션에 대한 상태의 이름을 지정하여 각기 다른 이름으로 생기는 혼란을 방지한다
(pending: 비동기 호출전, fulfilled: 비동기 호출성공, rejected: 비동기 호출실패)
export const fetchRecipes: any = createAsyncThunk<
any
>(
'recipes/fetchRecipes', // 액션 이름을 정의한다.
async () => { // 비동기 호출 함수를 정의
try {
const response = await fetch("https://www.themealdb.com/api/json/v1/1/search.php?s=")
const data = await response.json()
console.log("data: ", data)
return data.meals
} catch (error) {
}
},
);
위와 같이
createAsyncThunk를 선언하게 되면 첫번째 파라미터로 선언한 액션 이름 에pending,fulfilled,rejected의 상태에 대한 action 을 자동으로 생성해주게 된다.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
interface RecipeState {
loading: boolean,
hasErrors: boolean,
recipes: Array<RecipeData>,
}
export interface RecipeData {
idMeal: any,
strMeal: any
strMealThumb: any
}
const initialState: RecipeState = {
loading: false,
hasErrors: false,
recipes: [],
};
export const fetchRecipes: any = createAsyncThunk<
any
>(
'recipes/fetchRecipes',
async () => {
try {
const response = await fetch("https://www.themealdb.com/api/json/v1/1/search.php?s=")
const data = await response.json()
console.log("data: ", data)
return data.meals
} catch (error) {
//에러 처리 로직
}
},
);
export const recipesSlice = createSlice({
name: 'recipes',
initialState,
reducers: {
getRecipes: (state, { payload }) => {
state.recipes = payload
}
},
extraReducers: {
[fetchRecipes.pending]: (state) => {
state.loading = true
},
[fetchRecipes.fulfilled]: (state, { payload }) => {
state.recipes = payload
state.loading = false
state.hasErrors = false
},
[fetchRecipes.rejected]: (state) => {
state.loading = false
state.hasErrors = true
},
},
})
export const recipesSelector = (state: any) => state.recipes
export default recipesSlice.reducer
대박대박 감사합니다!!!