본 내용은 Redux 깃헙 내에 example 코드를 보며 공부한 내용을 바탕으로 작성했습니다.
Redux Toolkit
은 Redux에 대해 흔히 우려하는 세가지를 해결하기 위해 만들어졌습니다.
- 저장소를 설정하는 것이 너무 복잡하다.
- 쓸만하게 되려면 너무 많은 패키지들을 더 설치해야 한다.
- 보일러플레이트 코드를 너무 많이 필요로 한다. ( 설정해야 할 코드의 양이 많다로 해석 )
Redux Toolkit
이란 ?Redux
로직을 작성하기 위한 표준 방식이 되도록 만들어졌습니다.Redux Toolkit
안에는 저장소 준비, 리듀서 정의, 불변 업데이트 로직, 액션 생산자나 액션 타입을 직접 작성하지 않고도 전체 상태 '조각' 을 만들어내는 기능까지 대부분의 Redux
사용 방법에 해당하는 유틸리티 함수
들이 들어 있습니다.Redux Thunk
와 Selector 작성을 위한 Reselect
등의 널리 사용되는 애드온을 포함하고 있습니다.
- NPM
npm install @reduxjs/toolkit- Yarn
yarn add @reduxjs/toolkit
Redux
가 아닌 효율적인 개발을 위해 탄생한 Redux Toolkit
을 통해 Counter 프로젝트
를 한번 작성하고 분석해보겠습니다.
Redux Toolkit
을 사용할 때 2개의 함수만 잘 사용하면 됩니다. createSlice()
와 store의 구성설정인 configureStore()
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppThunk, RootState } from "../../app/store";
import { fetchCount } from "./CounterAPI";
export interface CounterState {
value: number;
status: "idle" | "loading" | "failed";
}
const initialState: CounterState = {
value: 0,
status: "idle",
};
// createAsyncThunk는 비동기 논리를 수행할 수 있게 해준다.
// 일반적으로 비동기 요청을 만드는데 사용한다.
export const incrementAsync = createAsyncThunk(
"counter/fetchCount",
async (amount: number) => {
const res = await fetchCount(amount);
return res.data;
}
);
// createSlice(): 조각 이름과 상태 초기값, 리듀서 함수들로 이루어진 객체를 받아 그에 맞는 액션 생산자와 액션 타입을 포함하는 리듀서 조각을 자동으로 만들어줍니다.
export const counterSlice = createSlice({
name: "counter",
initialState,
reducers: {
increment: (state) => {
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload;
},
},
// extraReducers필드를 사용하면 슬라이스가 다른 곳에서 정의된 작업을 처리할 수 있다.
extraReducers: (builder) => {
builder
.addCase(incrementAsync.pending, (state) => {
state.status = "loading";
})
.addCase(incrementAsync.fulfilled, (state, action) => {
state.status = "idle";
state.value += action.payload;
})
.addCase(incrementAsync.rejected, (state) => {
state.status = "failed";
});
},
});
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state: RootState) => state.counter.value;
export const incrementIfOdd =
(amount: number): AppThunk =>
(dispatch, getState) => {
const currentValue = selectCount(getState());
if (currentValue % 2 === 1) {
dispatch(incrementByAmount(amount));
}
};
export default counterSlice.reducer;
createSlice()
함수는 Parameter에 name
, initialState
, reducers
이렇게 3개를 작성하면 됩니다.
initialState
: default값이면서 동시에 상태관리에 사용되는 type, interface로 type지정을 하고, initialState의 값을 초기화 시켜줍니다. ( 현재 default 값 : '0' )name
:createSlice()
를 통해 slice를 생성하는데, 내부적으로중복
을 피하기 위해 사용되는 고유한 값입니다.reducers
: 상태변화를 처리하는 함수를 정의 합니다.
- 함수의 이름은
dispatch
로 부르는 액션 함수의 이름이며, 함수 내부는 위와 같이 state의 상태값을 변경하는 처리를 해줍니다.dispatch
에 포함해서 전달한 값은PayloadAction<>
의 타입의 action.payload 값으로 확인 할 수 있습니다.- 기존
Redux
에서 액션타입을 지정하고, 타입에 따른 액션 생성함수, action.type에 따른 상태 변화 처리 및 불변성 처리를 3단계에 나눠했다면,Redux Toolkit
에서는 이 하나의 함수를 정의하는 것으로 끝납니다.
코드를 먼저 살펴보면 아래와 같이 사용하면 됩니다.
import { Action, configureStore, ThunkAction } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
// store type 설정
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,
unknown,
Action<string>
>;
reducer
를 configureStore
에 등록시켜 줍니다. state.counter.value
를 사용해 store
에 저장된 reducer
의 값을 가져올 수 있습니다.import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";
import App from "./App";
import { store } from "./app/store";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
store
를 전역에서 사용할 수 있도록 App
시작지점에 Provider
를 등록해줍니다.import React, { useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import styles from "./Counter.module.css";
import {
decrement,
increment,
incrementAsync,
incrementByAmount,
incrementIfOdd,
selectCount,
} from "./counterSlice";
const Counter = () => {
const count = useAppSelector(selectCount);
const dispatch = useAppDispatch();
const [incrementAmount, setIncrementAmount] = useState("2");
const incrementValue = Number(incrementAmount) || 0;
return (
<div>
<div className={styles.row}>
<button
className={styles.button}
aria-label="Decrement value"
onClick={() => dispatch(decrement())}
>
-
</button>
<span className={styles.value}>{count}</span>
<button
className={styles.button}
aria-label="Increment value"
onClick={() => dispatch(increment())}
>
+
</button>
</div>
<div className={styles.row}>
<input
className={styles.textbox}
aria-label="Set increment amount"
value={incrementAmount}
onChange={(e) => setIncrementAmount(e.target.value)}
/>
<button
className={styles.button}
onClick={() => dispatch(incrementByAmount(incrementValue))}
>
Add Amount
</button>
<button
className={styles.asyncButton}
onClick={() => dispatch(incrementAsync(incrementValue))}
>
Add Async
</button>
<button
className={styles.button}
onClick={() => dispatch(incrementIfOdd(incrementValue))}
>
Add If Odd
</button>
</div>
</div>
);
};
export default Counter;
위 코드를 보는것과 같이 사용하는 방법은 간단합니다.
useSelector Hook
을 이용해 store에 저장된 state를 가져옵니다.
useDispatch
를 사용해 변경할 값을 reducer
에 전달해줍니다.
useSelector
와 useDispatch
는 따로 훅 컴포넌트로 사용했습니다.
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "./store";
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
Redux
보다 더 간결해지고, 직관적인 것을 확인 할 수 있다. Redux Toolkit
으로 프로젝트를 만들어서 내것으로 만들어야겠다.