
리덕스를 프로젝트에서 한 번 경험해 본 적이 있는데,
Redux-Toolkit은 처음 공부해본다 !
다음 프로젝트에 써보려면 열심히 정리해봐야겠지? 가보자
> Redux-Toolkit 공식 홈페이지
https://ko.redux.js.org/redux-toolkit/overview/

이는 위 링크의 Redux-Toolkit 팀의 공식 소개에도 잘 나와있다.
1. "저장소를 설정하는 것이 너무 복잡하다"
2. "쓸만하게 되려면 너무 많은 패키지들을 더 설치해야 한다"
3. "보일러플레이트 코드를 너무 많이 필요로 한다"
저는 여기서 보일러플레이트 코드라는 용어를 처음 접했는데,
의미는 다음과 같다.
컴퓨터 프로그래밍에서 보일러플레이트 또는 보일러플레이트 코드라고 부르는 것은 최소한의 변경으로 여러곳에서 재사용되며, 반복적으로 비슷한 형태를 띄는 코드를 말한다. - https://www.charlezz.com/?p=44186
action, initial state, reducer... 등등 Redux를 사용하기 위해 적어주어야 할 것들이 많고, 관리할 상태들이 많아지면서 이를 여러 번 적는 것이 귀찮았다! 이런 느낌인 것 같다.
그렇다면 Redux-Toolkit에서는 이런 문제들을 어떻게 개선했는지 보자!
기존 Redux에서는 다음과 같이 action value, action creator, reducer를
모두 적어줘야 했다.
const INCREMENT = "INCREMENT";
const DECREMENT = "DECREMENT";
//Action creator
export const onIncrement = (payload) => {
return {
type: INCREMENT,
payload,
};
};
export const onDecrement = (payload) => {
return {
type: DECREMENT,
payload,
};
};
// Initial State
const initialState = {
value: 0,
};
// Reducer
const counter = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return {
...state,
value: state.value + 1
}
case DECREMENT:
return {
...state,
value: state.value - 1
};
}
};
export default counter;
그러나 Redux-Toolkit에서는, Slice 라는 API를 사용하여
좀 더 편하고, 읽기 쉽게 해준다.
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import type { RootState } from '../../app/store'
// slice 타입 정의
interface CounterState {
value: number
}
// initial state
const initialState: CounterState = {
value: 0,
}
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
// payload 사용 시
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// RootState 타입을 사용하여 다른 코드(예: 셀렉터)에서 사용할 수 있도록
export const selectCount = (state: RootState) => state.counter.value
export default counterSlice.reducer
이 때 reducer 내부에 적힌 'increment', 'decrement'와 같은 이름이 action value의 이름으로 자동으로 생성된다.
ㆍname
ㆍinitialState : state 초기 상태 정의
ㆍreducers : 액션 설정
열심히 만든 리듀서를 하나로 병합해보자.
스토어는 하나 당 하나의 리듀서만을 가질 수 있기 때문에, 병합 과정이 꼭 필요하다.
// rootReducer.ts
import { combineReducers } from '@reduxjs/toolkit';
import counter from './counter/counterSlice';
// Store에 등록할 reducer들을 하나로 병합
const reducer = combineReducers({
counter,
// 이후 추가될 reducer들을 이곳에 추가
});
// reducer 타입을 미리 명시 후 내보내어야, 이후 다른 컴포넌트에서 상태 값을 가져올 수 있다
export type RootState = ReturnType<typeof reducer>;
export default reducer;
병합 후에는 스토어에 등록해준다.
// store.ts
import { configureStore } from '@reduxjs/toolkit';
import reducer from './rootReducer';
export const store = configureStore({
reducer,
});
export type AppDispatch = typeof store.dispatch;
export default store;
이후 전역에 뿌려주자!
// 최상위 index.tsx
import ReactDOM from 'react-dom/client';
import React from 'react';
import { App } from './App';
import { Provider } from 'react-redux';
import store from './store';
const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
);
// Counter.tsx
import React from 'react';
import { useAppSelector, useAppDispatch } from '../hooks';
import { decrement, increment } from '../counter/counterSlice';
export function Counter() {
// The `state` arg is correctly typed as `RootState` already
const count = useAppSelector((state) => state.counter.value);
const dispatch = useAppDispatch();
return (
<div>
<button aria-label="Decrement value" onClick={() => dispatch(decrement())}>
-
</button>
<span>{count}</span>
<button aria-label="Increment value" onClick={() => dispatch(increment())}>
+
</button>
</div>
);
}
기존의 reducer 같은 경우, 타입을 지정하지 않아도 작동에 문제가 되지는 않지만, RTK 공식문서에서는 AppDispatch, AppSelector 타입을 지정하는 것을 권장하고 있다.
// hooks.ts
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux';
import type { AppDispatch } from './store';
import type { RootState } from './rootReducer';
// Use throughout your app instead of plain `useDispatch` and `useSelector`
type DispatchFunc = () => AppDispatch;
export const useAppDispatch: DispatchFunc = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
확실히 리덕스보다는 쉬운 것 같다.
미들웨어, extraReducer 적용을 안해서 그런 걸지도..
프로젝트에 한 번 적용해보고 또 정리해봐야겠다.
그리고 요즘 핫한 Recoil, SWR도 사용, 정리해보고 어떤 상태 관리 툴을 쓸 지 더욱 고민해봐야겠다.