[React] Redux-toolkit

김채운·2023년 6월 23일
0

React

목록 보기
18/26

➡️ Redux Toolkit이란?

Redux Toolkit은 Redux의 사용을 더 쉽게 하기 위해 Redux에서 공식 제공하는 개발도구이다. 그에 따라 Redux Toolkit은 Redux에 대한

  • Redux 스토어 구성이 너무 복잡하다.
  • Redux가 유용한 작업을 수행하려면 많은 패키지를 추가해야 한다.
  • Redux에는 너무 많은 상용구 코드가 필요하다.

라는 세 가지 일반적인 문제를 해결하기 위해 만들어졌다.

👆 기존의 Redux

기존의 redux는 1개의 액션을 생성할 시 액션타입 정의 -> 액션함수 생성 -> 리듀서 정의 의 작업이 필요하다. 또 이 과정에서 필요에 의해 다양한 라이브러리가 사용이 되는데,
많아지는 액션을 관리하기 위한 redux-actions, 불변성을 지키기 위한 immer, 비동기 작업 수행을 위한 redux-thunk, redux-saga등 리덕스의 여러 기능 사용을 위해 여러 라이브러리를 사용해야 했다. 하지만 redux-toolkit은 redux-saga를 제외한 위의 모든 기능을 지원한다.
또, Typescript 사용자를 위해 action type, state type 등 Typescript 를 사용하며 필요한 Type Definition 까지 공식 지원한다.

👆 Reduxt toolkit 특징

1. Simple

  • 스토어 설정, 리듀서 생성, 불변 업데이트 로직 등과 같은 사용을 단순화하는 기능이 포함되어 있다.

2. Opinionated

  • 즉시 사용 가능한 스토어 설정을 위한 기본값을 제공하고 가장 일반적으로 사용되는 Redux Addon이 내장되어 있다.

3. Powerful

  • Immer같은 라이브러리에 영감을 얻어 "변형" 불변 업데이트 로직을 작성하고 state(상태) 전체 "슬라이스"로 자동으로 생성할 수 있다.

4. Effective

  • 앱에 필요한 핵심 논리에 집중할 수 있으므로 더 적은 코드로 더 많은 작업을 수행할 수 있다.

➡️ Redux Toolkit 설치하기.

1. Redux toolkit 및 React Redux 설치

npm install @reduxjs/toolkit react-redux

2. Redux가 이미 설치되어 있는 경우

npm install @reduxjs/toolkit

➡️ Redux Toolkit 사용하기.

1. store 생성

개인적으로 프로젝트를 진행할 때,
src/redux/configStore.ts 파일을 생성해서 Redux 저장소를 만들어줬다.

import { configureStore } from "@reduxjs/toolkit";
import productSlice from "./modules/productSlice";
import userSlice from "./modules/userSlice";
import cartSlice from "./modules/cartSlice";

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

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch;

export default store;

configureStore

  • 리덕스 라이브러리의의 createStore 함수를 추상화한 것입니다. 기존의 번거로웠던 리덕스 설정을 간편하게 할 수 있도록 해주고 설정시 디폴트로 redux-thunk와 DevTools를 제공해준다.
    또한 configureStore함수는 여러 개의 슬라이스(reducer)를 한 곳에 모아서 Redux 스토어(store)를 생성하는 역할을 합니다.

RootState

  • RootState는 스토어의 상태 타입을 정의하는 것으로, ReturnType를 사용하여 스토어의 상태 유형을 추론합니다.
  • store.getState()는 Redux 스토어의 상태를 반환하는 메서드이다. typeof store.getState()는 이 메서드의 반환 타입을 나타냅니다.ReturnType은 TypeScript에서 제공하는 유틸리티 타입 중 하나로, 함수의 반환 타입을 추출하는 역할을 합니다.
  • 따라서 ReturnType는 store.getState()의 반환 타입을 나타냅니다.

AppDispatch

  • AppDispatch는 스토어의 디스패치(dispatch) 타입을 정의하는 것으로, typeof store.dispatch를 사용하여 스토어의 디스패치 함수의 타입을 추론한다.

이렇게 설정된 Redux 스토어는 애플리케이션의 전역 상태 관리를 담당하며, store 변수를 통해 액션 디스패치(dispatch) 및 상태 조회 등의 기능을 사용할 수 있습니다. 리덕스 스토어를 설정함으로써 애플리케이션의 상태 관리를 편리하게 구현할 수 있다.

// src/hooks/reduxHooks.ts
  
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from "../redux/configStore";

export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
  • configureStore.js 파일에서 설정해둔 AppDispatch와 RootState를 사용해서 useAppDispatch, useAppSelector hook을 만들어줬다.
  • useDispatch와 useSelector는 React Redux 라이브러리에서 제공하는 기본 훅(Hook)이다. 그러나 기본 useDispatch,useSelector를 사용할 경우 타입스크립트에서 반환되는 값의 타입을 명시하기 어려울 수 있다.
  • 이렇게 커스텀 훅을 사용하면 명시적으로 타입 정보를 지정하여 컴포넌트에서 반환되는 값의 타입을 확실하게 할 수 있다.

useAppDispatch

  • useAppDispatch를 사용하면 반환되는 값의 타입을 명시적으로 AppDispatch로 지정할 수 있다. 이를 통해 useAppDispatch를 사용하는 컴포넌트에서는 타입스크립트가 반환되는 값의 타입을 정확히 추론하고, 따라서 타입 안정성을 확보할 수 있다.

useSelector

  • useAppSelector()를 호출하면 RootState 타입의 상태를 반환한다. TypedUseSelectorHook은 useSelector의 타입을 RootState로 지정하는 역할을 한다.

이러한 커스텀 훅들을 사용하면 Redux 스토어의 dispatch 메서드와 상태를 간편하게 사용할 수 있습니다. useAppDispatch를 사용하여 액션을 디스패치하고, useAppSelector를 사용하여 상태를 가져올 수 있다.

2. Redux store 적용

// index.tsx
  
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import store from "./redux/configStore";

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement);
root.render(
  <>
    <Provider store={store}>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </Provider>
  </>
);
  • 스토어가 생성되면 import 해와서 태그에 적용해주고 컴포넌트를 감싸준다.

3. Slice 생성하기.

// src/redux/modules/productSlice.ts
  
import { Product, ProductDetail } from "../../components/types/product";
import { apis } from "../../shared/api";
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

interface PropductInitProps {
    productOne: ProductDetail | null;
}

const initialState: PropductInitProps = {
    productOne: null
}

// 하나의 상품 가져오기
export const getOneProduct = createAsyncThunk(
    "product/getOneProduct",
    async (productId: number, thunkAPI) => {
        try {
            const res = await apis.getOneProduct(productId);
            return thunkAPI.fulfillWithValue(res.data)
        } catch (error) {
            console.log("상품하나", error);
            throw error;
        }
    }
)

const productSlice = createSlice({
    name: 'product',
    initialState,
    reducers: {},
    extraReducers: (builder) => {
        builder
            .addCase(getOneProduct.pending, (state) => {})
            .addCase(getOneProduct.fulfilled, (state, action) => {
                state.productOne = action.payload;
            })
            .addCase(getOneProduct.rejected, (state) => {})
    }
})
  • createSlice로 Slice를 만든다. 슬라이스를 생성하려면 슬라이스를 식별학기 위한 문자열 이름(name), 초기 상태 값(initialState), 상태 업데이트를 위한 하나 이상의 reducer 함수가 필요하다(reducers).
  • 위의 코드에서는 extraReducers를 사용했는데, reducers와의 차이점은 extraReducers를 사용하는 경우는 이미 다른 곳에서 정의된 액션생성함수를 사용할 때이다. 가장 흔한 케이스는 비동기를 위해 createAsyncThunk를 사용하여 정의된 액션함수를 사용하거나, 다른 slice에서 정의된 액션함수를 사용하는 경우이다.
  • reducers 는 액션함수를 생성함과 동시에 해당 액션함수에 대응하는 역할을 한다.
  • 위 코드의 경우에는 비동기를 위해 createAsyncThunk를 사용했기 때문에 extraReducers를 사용했다.

👆 createAsyncThunk

  • createAsyncThunk는 Redux Toolkit에서 제공하는 함수로, 비동기 작업을 처리하는 액션 생성자 함수를 간편하게 생성할 수 있도록 도와준다.
  • createAsyncThunk 함수를 사용하면 세 가지 액션 타입을 자동으로 생성할 수 있다:
    시작 액션(pending): 비동기 작업이 시작될 때 디스패치되는 액션.
    성공 액션(fulfilled): 비동기 작업이 성공적으로 완료될 때 디스패치되는 액션. 이 액션에는 비동기 작업의 결과 데이터가 포함될 수 있다.
    실패 액션(rejected): 비동기 작업이 실패했을 때 디스패치되는 액션. 이 액션에는 실패에 대한 정보, 예외 메시지 등의 에러 데이터가 포함될 수 있다.

createAsyncThunk를 사용하여 생성한 액션 생성자 함수는 비동기 작업을 수행하고, 작업이 진행 중인지, 성공했는지, 실패했는지에 따라 해당하는 액션을 자동으로 디스패치한다.
이를 통해 비동기 작업의 상태를 관리하고, 상태 업데이트에 따른 UI 변경을 처리하는 데 편리하게 사용할 수 있다.

4. store에 slice 추가하기.

import { configureStore } from "@reduxjs/toolkit";
import productSlice from "./modules/productSlice";

export const store = configureStore({
    reducer: {
      product: productSlice,
    },
});

export type RootState = ReturnType<typeof store.getState>
export type AppDispatch = typeof store.dispatch;

export default store;

5. React 컴포넌트 내부에서 리덕스 상태값 사용하기.

const dispatch = useAppDispatch()
const product = useAppSelector((state) => state.product.productOne)
  
useEffect(() => {
        dispatch(getOneProduct(finalId))
    }, [dispatch, finalId])

❗ reducers 사용 버전

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions
export default counterSlice.reducer

React 컴포넌트 내부에서 리덕스 상태값 사용하기.

import React from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment } from './counterSlice'

export function Counter() {
  // 데이터 추출
  const count = useSelector((state) => state.counter.value)
  // 액션 추출
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          onClick={() => dispatch(increment())}
        >
          Increment
        </button>
        <span>{count}</span>
        <button
          onClick={() => dispatch(decrement())}
        >
          Decrement
        </button>
      </div>
    </div>
  )
}

출저

0개의 댓글