[React] Redux toolkit

๐Ÿ“„ Redux toolkit

๋ฆฌ์•กํŠธ ํ™˜๊ฒฝ์—๋Š” ์ˆ˜๋งŽ์€ ์ƒํƒœ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•์ด ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฆฌ์•กํŠธ์— ์ด๋ฏธ ๋‚ด์žฅ๋˜์–ด ์žˆ๋Š” Context API๊ฐ€ ์žˆ์œผ๋ฉฐ, ์ด์™ธ์—๋„ ๋‹ค์–‘ํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค.

๊ทธ๋Ÿผ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ  ๋ฆฌ๋•์Šค๊ฐ€ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์‚ฌ์šฉ๋˜๊ณ  ์žˆ๋Š” ์ด์œ ๋Š” ๋ฌด์—‡์ผ๊นŒ์š”.

๐Ÿ“„ Context API vs Redux

์‚ฌ์‹ค Context API์™€ Redux๋Š” ๋น„๊ต๋Œ€์ƒ์ด ์•„๋‹™๋‹ˆ๋‹ค.

Context๋Š” ์ˆ˜๋‹จ์ผ ๋ฟ, ์ƒํƒœ๊ด€๋ฆฌ ์ž์ฒด๋Š” useState์™€ useReducer๊ฐ€ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค.

Context API์™€ Redux์˜ ๊ฐ€์žฅ ํฐ ์ฐจ์ด์ ์€ ์„ฑ๋Šฅ ์ตœ์ ํ™”์—์„œ ๋ณด์—ฌ์ง‘๋‹ˆ๋‹ค.

Context API๋Š” ์„ฑ๋Šฅ ์ตœ์ ํ™”๊ฐ€ ์ด๋ฃจ์–ด์ง€์ง€ ์•Š์•„ ํŠน์ •๊ฐ’์„ ์˜์กดํ•˜๋Š” ๊ฒฝ์šฐ, ํ•ด๋‹นํ•˜๋Š” ํŠน์ •๊ฐ’์ด ์•„๋‹Œ ๋‹ค๋ฅธ ๊ฐ’์ด ๋ณ€๊ฒฝ๋  ๋•Œ๋„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฉ๋‹ˆ๋‹ค.

๋ฐ˜๋ฉด Redux๋Š” ์˜์กดํ•˜์ง€ ์•Š๋Š” ๊ฐ’์ด ๋ฐ”๋€Œ๊ฒŒ๋˜๋ฉด ๊ทธ ๊ฐ’์— ์˜ํ–ฅ์„ ๋ฐ›์ง€ ์•Š๊ณ  ๋ฆฌ๋ Œ๋”๋ง์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.

๋”ฐ๋ผ์„œ Context API๋ฅผ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ์—๋Š” ์ปดํฌ๋„ŒํŠธ์˜ ๋ถ„๋ฆฌ๊ฐ€ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“„ Redux tookit ์‹œ์ž‘ํ•˜๊ธฐ

โ–ช Installation

โ–ช CRA๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ

# Redux + Plain JS template
npx create-react-app my-app --template redux

# Redux + TypeScript template
npx create-react-app my-app --template redux-typescript

โ–ช ์กด์žฌํ•˜๋Š” ์•ฑ์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒฝ์šฐ

# NPM
npm install @reduxjs/toolkit
# Yarn
yarn add @reduxjs/toolkit

๐Ÿ“„ Redux toolkit์— ์กด์žฌํ•˜๋Š” API

โ–ช configureStore()

  • createStores๋Š” ๊ฐ„๋‹จํ•œ configuration ์˜ต์…˜๋“ค๊ณผ ๊ธฐ๋ณธ๊ฐ’์„ ์ œ๊ณตํ•œ๋‹ค.
  • ์ž๋™์œผ๋กœ ์‚ฌ์šฉ์ž์˜ slice reducers๋ฅผ ํ•ฉ์ณ์„œ ์‚ฌ์šฉํ•˜๋Š” middleware๊ฐ€ ๋ฌด์—‡์ด๋“  ์ถ”๊ฐ€ํ•œ๋‹ค.

โ–ช createReducer()

  • ์ƒํƒœ๊ฐ€ ๋ฐ”๋€Œ๊ธฐ ์ „์— ์‚ฌ์šฉ์ž๊ฐ€ ๋ฃฉ์—… ํ…Œ์ด๋ธ”์„ ์ œ๊ณตํ•˜๊ฒŒ ํ•œ๋‹ค.

    lookup table? ์ฃผ์–ด์ง„ ์—ฐ์‚ฐ์— ๋Œ€ํ•ด ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ ๊ฒฐ๊ณผ๋“ค์˜ ์ง‘ํ•ฉ(๋ฐฐ์—ด)

  • ์ž๋™์œผ๋กœ immer๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๊ฐ„๋‹จํ•œ immutable ์—…๋ฐ์ดํŠธ๋ฅผ ์ž‘์„ฑํ•˜๊ฒŒ ํ•œ๋‹ค.

โ–ช createAction()

  • ์ฃผ์–ด์ง„ ์•ก์…˜ ํƒ€์ž…์— ๋Œ€ํ•ด stringํƒ€์ž…์œผ๋กœ ์•ก์…˜ํ•จ์ˆ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ๋‹ค.
  • ์ด ํ•จ์ˆ˜๋Š” toString()์ด ๋‚ด์žฅ๋˜์–ด ์žˆ์–ด, ์ƒ์ˆ˜ ํƒ€์ž…์„ ๋Œ€์‹ ํ•ด์„œ ์“ฐ์ธ๋‹ค.

โ–ช createSlice()

  • reducer ํ•จ์ˆ˜๋“ค, slice ์ด๋ฆ„, ์ดˆ๊ธฐ state ๊ฐ’์„ ํ—ˆ์šฉํ•˜์—ฌ ์ž๋™์œผ๋กœ slice reducer๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

โ–ช createAsyncThunk

  • string ํƒ€์ž…์˜ ์•ก์…˜๊ณผ ํ”„๋กœ๋ฏธ์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ›์•„๋“ค์ธ๋‹ค.
  • ์ž๋™์œผ๋กœ slice reducer๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

โ–ช createEntitiyAdapter

  • ์ผ๋ จ์˜ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฆฌ๋“€์„œ์™€ ์Šคํ† ์–ด ์•ˆ์— ๋ฐ์ดํ„ฐ๋ฅผ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ selectors๋ฅผ ์ƒ์„ฑํ•œ๋‹ค.

๐Ÿ“„ RTK Query

RTK Query๋Š” @reduxjs/toolkit ํŒจํ‚ค์ง€์— ์ถ”๊ฐ€๋˜์–ด ์žˆ๋Š” ๋ถ€๊ฐ€์ ์ธ ์˜ต์…˜์ž…๋‹ˆ๋‹ค.

API ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•˜๊ธฐ ์œ„ํ•ด ๋ฐ์ดํ„ฐ๋ฅผ fetchingํ•˜๊ณ  cachingํ•˜๋Š” ๊ฐ„๋‹จํ•˜์ง€๋งŒ ๊ฐ•๋ ฅํ•œ ํˆด์…‹์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“„ RTK Query์— ํฌํ•จ๋œ API๋“ค

โ–ช createApi()

  • RTK Query์˜ ํ•ต์‹ฌ์ ์ธ ๊ธฐ๋Šฅ
  • ๋ฐ์ดํ„ฐ๋ฅผ fetchingํ•˜๊ณ  ๋ณ€ํ™˜ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํฌํ•จํ•ด์„œ ์—”๋“œํฌ์ธํŠธ๋ฅผ ์ •์˜ํ•˜๊ณ  ์—”๋“œํฌ์ธํŠธ๋กœ๋ถ€ํ„ฐ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฒ€์ƒ‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํ—ˆ์šฉํ•œ๋‹ค.

    endpoint? ์ปดํ“จํ„ฐ ๋„คํŠธ์›Œํฌ์— ์—ฐ๊ฒฐํ•˜๊ณ  ์ปดํ“จํ„ฐ ๋„คํŠธ์›Œํฌ์™€์ •๋ณด๋ฅผ ๊ตํ™˜ํ•˜๋Š” ๋ฌผ๋ฆฌ์  ๋””๋ฐ”์ด์Šค

โ–ช fetchBaseQuery()

  • ์š”์ฒญ์„ ๋‹จ์ˆœํ™”ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜๋Š” fetch๋ฅผ ๊ฐ์‹ผ๋‹ค. = ๋ฐ์ดํ„ฐ fetching์„ ๋‹จ์ˆœํ™”ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋œ๋‹ค.
  • Redux toolkit ๊ณตํ™ˆ์€ createApi์—์„œ baseQuery๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•œ๋‹ค.

โ–ช ApiProvider

  • ์•„์ง Redux Store๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค.

โ–ช setupListeners()

  • refetOnMount์™€ refetchOnReconnect๋ฅผ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ

๐Ÿ“„ ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ์™€ RDK ์‹œ์ž‘ํ•˜๊ธฐ

โ–ช Redux Toolkit ์ดˆ๊ธฐ ์„ค์ •

๐Ÿ’พ app/store.ts

import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

export type AppDispatch = typeof store.dispatch;
// ํƒ€์ž… ์—๋Ÿฌ๋ฅผ ๋ง‰๊ธฐ ์œ„ํ•ด ์Šคํ† ์–ด ์„ค์ • ํŒŒ์ผ์—์„œ ์ง์ ‘ ๋‚ด๋ณด๋‚ด๊ณ ๋‹ค๋ฅธ ํŒŒ์ผ๋กœ ์ง์ ‘๊ฐ€์ ธ์˜ค๋Š” ๊ฒƒ์ด ์•ˆ์ „ํ•˜๋‹ค.
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
  ReturnType,
  RootState,
  unknown,
  Action<string>
>;

๐Ÿ’พ app/hooks.ts

์‚ฌ์šฉํ•˜๋Š” ํ›…๋“ค์˜ ํƒ€์ž…์„ ์ง€์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. RootState์™€ AppDispatch์˜ ํƒ€์ž…์€ ๊ฐ๊ฐ ์ปดํฌ๋„ŒํŠธ์—์„œ importํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๊ฐ€๋Šฅํ•˜์ง€๋งŒ,

useDispatch์™€ useSelector๋Š” ํƒ€์ž…์ด ์ง€์ •๋œ ํ›…์œผ๋กœ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ข‹์Šต๋‹ˆ๋‹ค.

  • useSelector์˜ ๊ฒฝ์šฐ, ๋งค๋ฒˆ (state: RootState)์˜ ํƒ€์ž…์„ ์ง€์ •ํ•ด์ค„ ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
  • useDispatch์˜ ๊ฒฝ์šฐ, ๊ธฐ๋ณธ Dispatch๋Š” thunk๋ฅผ ์•Œ์ง€๋ชปํ•ด์„œ thunk middleware ํƒ€์ž…์ด ํฌํ•จ๋œ ์Šคํ† ์–ด์—์„œ ์ปค์Šคํ„ฐ๋งˆ์ด์ง•๋œ AppDispatch ํƒ€์ž…์„ ์‚ฌ์šฉํ•ด์•ผํ•ฉ๋‹ˆ๋‹ค. useDispatch๋ฅผ ์ถ”๊ฐ€ํ•˜๋ฉด ํ•„์š”ํ• ๋•Œ AppDispatch๋ฅผ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ด ํ›…๋“ค์€ ํƒ€์ž…์ด ์•„๋‹ˆ๋ผ ๋ณ€์ˆ˜์ด๊ธฐ ๋•Œ๋ฌธ์— store ํŒŒ์ผ์ด ์•„๋‹ˆ๋ผ hooksํŒŒ์ผ์— ์ง€์ •ํ•ด์•ผํ•œ๋‹ค. ์ด๋ ‡๊ฒŒ hooksํŒŒ์ผ์— ์ง€์ •ํ•ด์„œ ํ•„์š”ํ• ๋•Œ ๋งˆ๋‹ค componentํŒŒ์ผ์—์„œ importํ•ด์˜ฌ ์ˆ˜ ์žˆ๋‹ค.

import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

๐Ÿ“„ Application ์‚ฌ์šฉ

โ–ช Slice State์™€ Action Types

๊ฐ๊ฐ slice ํŒŒ์ผ์€ ์ดˆ๊ธฐ state value์— ๋Œ€ํ•œ ํƒ€์ž…์„ ์ง€์ •ํ•ด์„œ createSlice๊ฐ€ ๊ฐ๊ฐ ๋ฆฌ๋“€์„œ์˜ state์— ๋Œ€ํ•œ ํƒ€์ž…์„ ์ถ”๋ก ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

initial state type์„ ๋ฏธ๋ฆฌ ์ง€์ •ํ•˜๊ณ  initial state ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.

// slice state ํƒ€์ž… ์ •์˜
export interface CounterState {
  value: number;
  status: "idle" | "loading" | "failed";
}

// initial state ๊ฐ์ฒด ์ƒ์„ฑ
const initialState: CounterState = {
  value: 0,
  status: "idle",
};

โ–ช createSlice๋กœ slice ์ƒ์„ฑ

createSlice๋Š” name, initialState, reducers๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

  • name: action์•ž์— ๋ถ™์–ด ๋‹ค๋ฅธ slice์˜ action๋“ค๊ณผ ์ค‘๋ณต์„ ํ”ผํ•œ๋‹ค.
  • initialState: ๋ฏธ๋ฆฌ ์ƒ์„ฑํ•œ initialState๊ฐ€ ๋“ค์–ด์žˆ๋‹ค.
  • reducer: reducer๋Š” action ์—ญํ• ์„ ํ•˜๊ณ  state์˜ ๋ณ€ํ™”๋ฅผ ๋‹ด๋‹นํ•œ๋‹ค. immer.js๋ฅผ ๋‚ด์žฅํ•˜๊ณ  ์žˆ์–ด state๊ฐ’์„ ์ž๋™์œผ๋กœ returnํ•œ๋‹ค.
const counterSlice = createSlice({
  name: "counter",
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    incrementByValue: (state, action: PayloadAction<number>) => {
      state.value += action.payload;
    },
  },
});

โ–ช export

slice ๋‚ด์˜ actions๊ณผ reducer๋ฅผ exportํ•ฉ๋‹ˆ๋‹ค.

// export actions
export const { increment, decrement, incrementByValue } = counterSlice.actions;
// export default slice.reducer
export default counterSlice.reducer;

โ–ช Store ์ƒ์„ฑ

store์—๋Š” state์™€ dispatchํ•  ํ•จ์ˆ˜๋“ค์ด ๋“ค์–ด์žˆ์Šต๋‹ˆ๋‹ค.

ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๋ฅผ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋ฉด ๊ฐ๊ฐ state์™€ dispatch์˜ ํƒ€์ž…์„ ์ง€์ •ํ•ด ์ฃผ์–ด์•ผ ํ•œ๋‹ค.

  • store ์ƒ์„ฑ ๋ช…๋ น์–ด: configureStore
import { configureStore } from "@reduxjs/toolkit";
import CounterReducer from "../features/counter/counter";

export const store = configureStore({
  reducer: {
    counter: CounterReducer,
  },
});

// store์™€ dispatch์˜ ํƒ€์ž…
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

โ–ช Provider ์ƒ์„ฑ

Provider์€ store์™€ app์„ ์—ฐ๊ฒฐํ•ด์„œ ์ปดํฌ๋„ŒํŠธ๋“ค์ด store์— ์žˆ๋Š” state๋‚˜ dispatch๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€์žฅ ์ƒ์œ„ ์ปดํฌ๋„ŒํŠธ์— store๋ฅผ ์—ฐ๊ฒฐํ•˜๋ฉด ํ•˜์œ„ ์ปดํฌ๋„ŒํŠธ์—์„œ๋„ store๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

// index.tsx
import { Provider } from "react-redux";
import { store } from "./app/store";

ReactDOM.render(
  <React.StrictMode>
    <Provider store={store}>
      <App />
    </Provider>
  </React.StrictMode>,
  document.getElementById("root")
);

โ–ช ์ปดํฌ๋„ŒํŠธ ์•ˆ์—์„œ ์‚ฌ์šฉํ•˜๊ธฐ

import React, { useState } from "react";
import { useAppDispatch, useAppSelector } from "../../app/hooks";

import { increment, decrement, incrementByValue } from "./counter";

function CounterView() {
  // ์„ค์ •ํ•œ hook๋“ค์„ ์ ์šฉ
  const count = useAppSelector((state) => state.counter.value);
  const dispatch = useAppDispatch();
  // useState๋กœ num๊ฐ’ ๊ด€๋ฆฌ
  const [num, setNum] = useState<number>(0);
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setNum(parseInt(e.currentTarget.value));
  };

  return (
    <>
      <div
        style=
      >
        // ๋ฒ„ํŠผ์ด ๋ˆŒ๋ฆฌ๋ฉด importํ•ด์˜จ ํ•จ์ˆ˜๋“ค์ด dispatch๋œ๋‹ค.
        <button onClick={() => dispatch(decrement())}>-1</button>
        <h1 style=>{count}</h1>
        <button onClick={() => dispatch(increment())}>+1</button>
      </div>

      <div>
        <input type="number" onChange={handleChange} />
        <button onClick={() => dispatch(incrementByValue(num))}>+{num}</button>
      </div>
    </>
  );
}
export default CounterView;

์ฐธ๊ณ 

Leave a comment