오늘은 리덕스 툴킷에 대해 알아보자~!
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
대박 도움되었습니다!