최근 한 프로젝트로 redux toolkit과 saga를 함께 사용하여야하는 상황이 생겼다. recoil을 알게된 이후로는 세팅할 수 있는 상황이라면 redux보다는 recoil을 사용해서 redux에 대해 잘 모르고 있었는데 이번 기회로 정리하면서 공부해보려고 한다.
먼저 redux란 현재 가장 활발하게 사용되고 있는 상태관리 라이브러리이다.
recoil, jotai처럼 편리하게 사용할 수 있는 라이브러리들이 많이 나왔고 신규 프로젝트의 경우 이러한 라이브러리를 사용하는 곳이 많아졌지만 아직도 redux는 가장 활발하게 사용되고 있고, 예전에 작업한 프로젝트의 유지보수를 위해서라면 redux에 대해서도 꼭 알고 있어야 한다.
redux를 보다 편리하게 작성하기 위해 개발된 툴킷으로 공식 홈페이지에서도 RTK(redux-Toolkit)를 사용하는것을 권장한다.
redux를 사용할때는 불편한 것을 해결하기 위해 많은 라이브러리를 함께 사용하게 된다. 매번 액션을 하나하나 만들게 되면 너무 많은 코드가 생성되니 redux-actons을 사용하게 되고, 불변성을 지켜야하는 원칙 때문에 immer를 사용하게되고, store 값을 효율적으로 핸들링하여 불필요 리렌더링을 막기 위해 reselect를 쓰고, 비동기를 수월하게 하기위해 thunk나 saga를 설치하여 redux를 더 효율적으로 사용하게 된다.
즉 redux를 사용해서 상태관리를 하기 위해서는 수많은 라이브러리와 많은 양의 코드가 필요하다는 것이다.
그런데, redux-toolkit은 saga를 제외한 위 기능이 거의 모두 지원한다.
아래와 같은 리덕스의 문제점을 보완해준다.
store를 생성할때 사용된다.
기존 Redux에서는 Reducer를 생성하고 combineReducers로 여러 Reducer를 합쳐준 rootReducer를 만들고 그 다음 createStore로 store를 만들어 줘야 했다.
export const rootReducer = combineReducers({
counter: counterReducer,
todoList: todoReducer,
});
...
const store = createStore(rootReducer);
반면 configureStore를 사용하면 reducer를 객체형식으로 전달하여 바로 만들 수 있다.
const store = configureStore({
reducer: {
counter: counterSlice.reducer,
todoList: todoSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger).concat(sagaMiddleware),
devTools: process.env.NODE_ENV !== "production",
});
Store는 reducer를 객체 형식으로 전달하여 만들 수 있다.
createStore와 비슷하지만 {reducer: rootReducer}로 작성해야 한다는 차이점이 있다.
제공하는 모든 Redux 미들웨어를 추가하고, redux-thunk기본적으로 포함하고, Redux DevTools Extension을 사용할 수 있다.
리듀서 함수의 객체, 슬라이스 이름, 초기 상태 값을 받아 해당 액션 생성자와 액션 타입으로 슬라이스 리듀서를 자동으로 생성(name+action name)한다.
const comment = createSlice({
name: "comment",
initialState,
reducers: {
//일반action
handlePage: (state, action) => {
state.data.page = action.payload;
},
//비동기작업(saga)
}
extraReducers:{
//비동기작업(thunk)
}
});
export const commentActions = comment.actions;
export default comment.reducer;
RTK에서는 comment의 actions를 export해 각각의 action에 접근할 수 있다.
state.data.list = action.payload;
이때 actions 내에서는 immer를 기본적으로 제공하기 때문에 필요한 부분만 수정해주어도 되어 간편하다.
redux-toolkit에서는 react-thunk가 기본적으로 제공이 되는데 createAsyncThunk를 사용하면 된다.
export const getComment = createAsyncThunk(
'comment/get',
async (, thunkAPI) => {
try {
const response = await axios.get('/comments')
return response.data
} catch (error) {
return thunkAPI.rejectWithValue(error)
}
}
)
이렇게 만든 액션은 리듀서에 연결할때는 reducers가 아닌 extraReducers에 연결해주어야 한다.
const comment = createSlice({
name: "comment",
initialState,
reducers: {
}
extraReducers:(builder)=>{
builder.addCase(getComment.pending,()=>{
})
.addCase(getComment.fulfilled,()=>{
})
.addCase(getComment.rejected,()=>{
})
}
});
extraReducers에서 파라미터인 builder의 addCase를 사용해 각각의 case에 따른 처리를 등록해주어야 한다.
이때 case는 pending(대기), fulfilled(성공), rejected(실패) 3가지가 있다.
설치
//redux설치
npm install redux react-redux
//toolkit 설치
npm install @reduxjs/toolkit
//로그 확인을 위한 미들웨어 logger설치
npm install logger
store생성
/src/store/store.js
import { configureStore } from "@reduxjs/toolkit";
import { createLogger } from "redux-logger";
import commentReducer from "./reducer";
function createStore() {
const store = configureStore({
reducer: {
comment: commentReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(logger),
devTools: process.env.NODE_ENV !== "production",
});
return store;
}
const store = createStore();
export default store;
reducer
/src/store/reducer.js
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
const initialState = {
loading: false,
error: null,
data: {
list: [],
edit: null,
limit: 4,
page: 1,
}
};
export const getComment = createAsyncThunk(
'comment/get',
async (, thunkAPI) => {
try {
const response = await axios.get('/comments')
return response.data
} catch (error) {
return thunkAPI.rejectWithValue(error)
}
}
);
const comment = createSlice({
name: "comment",
initialState,
reducers: {
findComment(state, action) {
const item = state.data.list.find((item) => item.id === action.payload);
state.data.edit = item;
}
},
extraReducers:(builder)=>{
builder.addCase(getComment.pending,(state)=>{
state.loading = true;
})
.addCase(getComment.fulfilled,(state, action)=>{
state.loading = false;
state.data.list = action.payload;
})
.addCase(getComment.rejected,(state, action)=>{
state.loading = false;
state.data = null;
state.error = action.payload;
})
}
});
export const commentActions = comment.actions;
export default comment.reducer;