Redux-Toolkit을 적용해보자.
Redux는 가장 먼저 접하고 사용했던 상태 관리 라이브러리이다. 당시 saga와 함께 쓰면서 어마어마한 보일러 플레이트를 경험했었다. 흔히들 말하는 리덕스의 최대 단점이기도 한데 한번 익숙해지고 나서는 나름 체계적인 것 같기도 하고 크게 불편함을 느끼지 못했다. 게다가 이를 보완한 Redux-Toolkit은 확실히 간편해졌다. 물론, 아무리 간편해져도 Context나 Recoil에 비하면 여전히 번거롭기는 하다. 그래도 첫인상이 좋았던 덕인지, 익숙해져서인지 리덕스로 상태 관리를 했을 때 전체적인 상태 관계들이 더 잘보이는 것 같아서 장점이 더 크게 느껴진다.
Redux-Toolkit의 실행 구조를 그림으로 확인해보자.
ContextAPI와 비교해보면 RTK의 slice는 useState가 적용된 Context Provider 같다. Store는 여러개의 클라우드를 가지고 있다. 사용할 때, 어떤 클라우드를 사용할 것인지만 선택하면 된다.
1. store 생성
import { configureStore } from "@reduxjs/toolkit";
const store = configureStore({
reducer: {
슬라이스이름: 생성한슬라이스.reducer,
},
});
export default store;
store는 1개이다. 프로젝트의 규모가 커질수록 slice를 분리하고, slice들을 1개의 store에 저장한다. store의 reducer는 쪼개진 슬라이스들이 가지고 있는 reducer를 불러온다.
2. slice 생성
const sectionSlice = createSlice({
name: "section",
initialState: sectionList,
reducers: {
addSection: (state, action) => {
const { inputType, name, title } = action.payload;
//state 변경
},
...
},
});
slice는 name, initialState, reducers들을 갖는다. 컴포넌트에서 action 함수로부터 전달 받은 인자들은 action.payload
안에 담겨 있다. 1개 이상일 경우 구조분해하면 깔끔하다.
✨ RTK는 자체적으로 immer를 적용하고 있어 불변성을 고려하지 않아도 된다.
3. Provider 적용하기
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
import { Provider } from "react-redux";
import store from "./store/configureStore";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<Provider store={store}>
<App />
</Provider>
);
이제 모든 컴포넌트들은 매번 Provider를 만들 필요 없이 1개의 store에서 자유롭게 데이터를 꺼내서 사용할 수 있다. Context와 달리 최상위 컴포넌트를 1개의 클라우드로 감싸주면 되는 부분이 깔끔하게 느껴진다.
4. state 불러오기
///CheckList.jsx
const sectionList = useSelector((state) => state.section);
useSelector는 리덕스 스토어의 상태를 조회한다. 스토어에 저장된 상태들을 받아와서 그 중에서 section이라는 slice의 state값을 리턴한다. 여러개의 슬라이스가 있다면 그 중에서 원하는 state를 갖고 있는 슬라이스를 선택하는 것이다.
useSelector가 조회하는 slice의 이름은 store의 reducer에 지정한 이름이다.
위 코드를 풀어서 작성하면 아래와 같다.
const sectionList = useSelector((state) => {
return state.section;
state(sectionList)가 객체일 때, 구조분해로 가져올 경우 불필요한 리렌더링(Context의 리렌더링 문제와 비슷한)이 발생한다. 이는 원하는 값마다 useSelector로 불러오는 것으로 해결할 수 있다.
5. action 불러오기
const dispatch = useDispatch();
//호출
dispatch(sectionSlice.actions.addSection({ inputType, name, title }));
slice의 reducers들은 actions에 담겨있다. 이를 사용하기 위해서는 useDispatch를 이용한다. 작성 방식은 useReducer의 dispatch와 동일하다. 이 코드는 slice에서 export 할 때 더 간단하게 할 수 있다.
//sectionSlice.js
export const { addItem } = sectionSlice.actions;
reducers의 action 함수만 구조분해를 이용해 export하고 컴포넌트에서는 함수를 바로 호출한다.
//초기값
const initialState = {
count: 0
}
//Action
export const incrementAction = (count) => {
return {
type: 'increment',
count
}
}
//Reducer
export default (state = initialState, action) => {
switch (action.type) {
case 'increment': {
return state + action.count
}
//Component에서 값 사용
const count = useSelector((state) => state.count)
//Component에서 값 수정
dispatch(incrementAction(1))
Redux의 Store는 프로젝트에서 관리되는 모든 reducer들을 하나의 객체로 가지고 있다.
실행순서
- dispatch 함수를 호출하면서 action 함수를 실행한다.
- action 함수는 인자를 받아
{type: 'increment', count}
를 반환한다.- dispatch 함수의 호출은 reducer 호출과 같다.
2번의 반환값을 store에 저장된 reducer가 받아 type에 일치하는 함수를 실행한다.
1.createAction
export const increment = createAction(type, prepareAction?); //type : counter/increase
console.log(increment()) // {type: counter/increase}
console.log(increment(1)) // {type: counter/increase, payload: 1}
외부에서 호출되는 action 함수를 생성해준다. 이때 type은 필수값이지만, 선택적으로 두번째 인자에 콜백함수를 전달할 수 있다.
createAction으로 생성되는 것은 함수로 호출하여 사용한다.
2.createReducer
const initialState = {
count: 0
}
const counterReducer = createReducer(initialState, (builder) => {
builder
.addCase(increment, (state, action) => {
state + action.payload
})
})
Redux에서 reducer type 구분을 위한 switch문을 대체한다. addCase의 첫번째 인자는 1번에서 만든 action함수이다. 컴포넌트에서의 사용은 useSelector와 dispatch를 이용한다는 점은 동일하다.
createSlice는 createAction + createReducer와 같다.
별도 action을 생성하지 않아도 slice에 설정한 reducer 함수가 곧 외부에서 사용되는 액션 함수가 된다.