일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- javascript
- React
- Gin
- springboot
- storybook
- Chakra
- satisfiles
- RTK
- css
- 티스토리챌린지
- frontend
- Spring
- JPA
- Redux
- hook
- component
- designpatterns
- JavaSpring
- tanstackquery
- golang
- go
- 웹애플리케이션서버
- test
- java
- react-hook-form
- 오블완
- typescript
- ReactHooks
- backend
- Today
- Total
bkdragon's log
[Redux] Redux Toolkit 본문
Redux Toolkit
Redux를 편하게 사용할 수 있는 도구이다. Redux의 보일러 플레이트 코드를 줄이고 immer, reselect, redux-thunk 등의 부가 라이브러리를 통해 편의성을 증가시켰다.
우선 얼마나 코드가 줄어드는지 확인 해보자.
기존 코드
oldCounter.ts
// 액션 타입에 들어갈 값
const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
const INCREASE_BY = 'counter/INCREASE_BY' as const;
// 액션 생성 함수
export const increase = () => ({
type: INCREASE,
});
export const decrease = () => ({
type: DECREASE,
});
export const increaseBy = (diff: number) => ({
type: INCREASE_BY,
payload: diff,
});
// 액션의 타입
type CounterAction =
| ReturnType<typeof increase>
| ReturnType<typeof decrease>
| ReturnType<typeof increaseBy>;
// 상태 타입
type CounterState = {
count: number;
};
// 초기상태를 선언합니다.
const initialState: CounterState = {
count: 0,
};
function counter(
state: CounterState = initialState,
action: CounterAction
): CounterState {
switch (action.type) {
case INCREASE:
return { count: state.count + 1 };
case DECREASE:
return { count: state.count - 1 };
case INCREASE_BY:
return { count: state.count + action.payload };
default:
return state;
}
}
export default counter;
이랬던 코드가 다음과 같이 바뀐다.
counter.ts
import { createSlice, createAction } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
interface CounterState {
value: number;
}
const initialState: CounterState = {
value: 0,
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementBy: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
});
export const { increment, decrement, incrementBy } = counterSlice.actions;
export default counterSlice.reducer;
코드가 이렇게 줄어든 이유는 별도의 action 생성 함수가 따로 없기 때문이다. 기존 redux는 dispatcher가 action 생성 함수를 통해 type과 payload를 속성으로 가지는 객체를 생성해서 스토어(리듀서)로 보내주는데 redux-toolkit에서는 dispatcher가 바로 리듀서를 실행시키고 이때 type은 자동으로 생성되고, payload는 리듀서의 인자를 통해 받은 값이 된다.
- name : 모듈의 이름을 나타낸다.
- initialState : state의 초기 값을 설정한다.
- reducers : reducer를 작성한다, reducer의 인자로 기존의 action 생성 함수가 생성하던 action이 들어가 있다.
- PayloadAction<T> : Payload의 타입을 정의할 수 있다. = reducer의 인자의 타입을 정의할 수 있다.
store의 생성도 엄청 간단해진다. 기존의 코드를 보면
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import promiseMiddleware from 'redux-promise';
import ReduxThunk from 'redux-thunk';
import rootReducer from './_reducers/index'; // 리듀서를 하나로 합친것
const createStoreWithMiddleware = applyMiddleware(
promiseMiddleware,// store에서 promise를 다룰 수 있게 해준다.
ReduxThunk // store에서 함수를 다룰 수 있게 해준다.
)(createStore); // store를 생성하는 코드
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider
store={createStoreWithMiddleware(
rootReducer,
composeWithDevTools() // redux dev tool
)}
>
<App />
</Provider>
</React.StrictMode>
);
추가적인 라이브러리를 받아와서 그것들을 합쳐서 store를 만드는데
store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { useDispatch, useSelector } from 'react-redux';
import countReducer from './reducers/counter';
import type { TypedUseSelectorHook } from 'react-redux';
const store = configureStore({
reducer: {
counter: countReducer,
},
});
// state type
export type RootState = ReturnType<typeof store.getState>;
export default store;
간단하게 만들고,
import { Provider } from 'react-redux';
import store from './store/index';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
간단하게 적용시킨다.
Immer
flux 패턴의 주요 개념 중 하나는 불변성 유지이다.
redux를 사용해보았다면 스프레드 연산자로 복사본을 만들어 불변성을 유지하는 업데이트를 작성하는 것을 알고 있고 데이터가 충접 될수록 복잡해지는 경험도 있을 것이다. 그런데 createSlice의 reducer를 보면 상태를 직접적으로 변경하는 것을 볼 수 있다. 사실 이것은 내부에서 immer가 불변성을 유지하면서 상태를 업데이트하게 도와준다.
이때 몇가지 주의 사항이 있다. 예시와 함께 살펴보자.
- 기존처럼 새 상태를 반환하는 방법을 사용해도 되지만, 새 상태 반환과 직접적인 값 변경을 같이 사용해서는 안된다.
reducers: {
brokenReducer: (state, action) => state.push(action.payload), // ❌
fixedReducer2: (state, action) => { // ✅
state.push(action.payload)
},
. 2. 상태를 아예 대체해서는 안된다.
reducers: {
brokenTodosLoadedReducer(state, action) { // ❌
state = action.payload
},
},
3. 원시값 변경
immer가 배열이나 객체에만 적용되기 때문 !
reducers: {
brokenTodoToggled(state, action) {
const todo = state.find((todo) => todo.id === action.payload)
if (todo) {
let { completed } = todo
completed = !completed // ❌
}
},
fixedTodoToggled(state, action) {
const todo = state.find((todo) => todo.id === action.payload)
if (todo) {
todo.completed = !todo.completed // ✅
}
},
},
타입화된 훅 정의하기.
각 컴포넌로 RootState 및 AppDispatch의 타입을 가져올 수도 있지만, 타입화된 훅을 정의할 수도 있다. 타입화된 훅을 사용하면 useSelector의 경우 매번 타입을 입력할 필요가 없어진다. useDispatch의 경우 middleware를 고려하는 타입이 없기 때문에 store에서 제공하는 AppDispatch 타입을 useDispatch와 함께(제네릭으로) 사용해야 한다.
Dispatch의 타입
store를 생성하는 파일에 추가해주자.
// Dispatch 타입
export type AppDispatch = typeof store.dispatch;
export const useAppDispatch = () => useDispatch<AppDispatch>();
useSeletor의 타입
마찬가지로 같은 위치에 추가해주자.
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
공식 문서의 내용을 위주로 작성된 글이다.
'concept' 카테고리의 다른 글
CORS 요리 (0) | 2023.07.29 |
---|---|
Promise 병렬로 처리하기 (1) | 2023.07.29 |
[Cookie] 내가 만든 Cookie (0) | 2023.07.26 |
JavaScript 엔진이 await를 만났을 때 (0) | 2023.07.18 |
Flux 패턴 (0) | 2023.07.17 |