Redux-example 파헤쳐보기

ChangHyeon Bae·2023년 1월 18일
0

JavaScript

목록 보기
6/8
post-thumbnail

본 내용은 Redux 깃헙 내에 example 코드를 보며 공부한 내용을 바탕으로 작성했습니다.

📖 Redux Toolkit

💡 Redux Toolkit은 Redux에 대해 흔히 우려하는 세가지를 해결하기 위해 만들어졌습니다.

  1. 저장소를 설정하는 것이 너무 복잡하다.
  2. 쓸만하게 되려면 너무 많은 패키지들을 더 설치해야 한다.
  3. 보일러플레이트 코드를 너무 많이 필요로 한다. ( 설정해야 할 코드의 양이 많다로 해석 )

💡 위와 같은 문제점을 개선한 Redux Toolkit 이란 ?

  • Redux 로직을 작성하기 위한 표준 방식이 되도록 만들어졌습니다.
  • Redux Toolkit 안에는 저장소 준비, 리듀서 정의, 불변 업데이트 로직, 액션 생산자나 액션 타입을 직접 작성하지 않고도 전체 상태 '조각' 을 만들어내는 기능까지 대부분의 Redux 사용 방법에 해당하는 유틸리티 함수 들이 들어 있습니다.
  • 비동기 로직을 위한 Redux Thunk 와 Selector 작성을 위한 Reselect등의 널리 사용되는 애드온을 포함하고 있습니다.
  • 기본 동작을 제공하고, 실수를 줄여주고, 더 간단한 코드를 작성하게 해줍니다. ( 좋은 Redux앱 개발에 도움 )

💡 설치 방법

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

Redux가 아닌 효율적인 개발을 위해 탄생한 Redux Toolkit을 통해 Counter 프로젝트 를 한번 작성하고 분석해보겠습니다.

  • Redux Toolkit 을 사용할 때 2개의 함수만 잘 사용하면 됩니다.
  • createSlice() 와 store의 구성설정인 configureStore()

💡 createSlice() 알아보기

/features/counter/counterSlice.ts

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;
  • 위 코드는 카운트를 증감하는 예제 (Counter) 입니다.
  • 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에서는 이 하나의 함수를 정의하는 것으로 끝납니다.

💡 configureStore()

코드를 먼저 살펴보면 아래와 같이 사용하면 됩니다.

app/store.ts

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>
>;
  • reducerconfigureStore 에 등록시켜 줍니다.
  • 추후에 App에서 state.counter.value를 사용해 store에 저장된 reducer 의 값을 가져올 수 있습니다.

💡 App 전역에 Provider 등록

index.tsx

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를 등록해줍니다.

💡 store 호출하여 dispatch

feature/counter/Counter.tsx

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에 전달해줍니다.

  • useSelectoruseDispatch는 따로 훅 컴포넌트로 사용했습니다.

hooks.ts

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으로 프로젝트를 만들어서 내것으로 만들어야겠다.

🔗 참고

profile
모든 결과는 내가 하기 나름이다 🔥

0개의 댓글