본문 바로가기

자기계발

Redux Toolkit으로 전역상태 관리하기

Redux Toolkit은 Redux의 사용을 더욱 간편하고 효율적으로 만들어주는 공식 툴셋입니다. 기존 Redux의 복잡한 설정과 반복적인 코드를 줄여주며, 쉽게 전역 상태 관리를 할 수 있도록 도와줍니다. 

이번 글에서는 Redux Toolkit의 개념과 주요 기능, 그리고 사용법을 알아보겠습니다.

 

• Redux Toolkit이란 무엇인가요 ?

Redux Toolkit은 Redux의 공식 툴킷으로, 전역 상태 관리를 더욱 쉽게 하고 보일러플레이트 코드를 줄여주는 라이브러리입니다. Redux의 기본 개념은 그대로 유지하면서도 사용성을 크게 개선하여, 개발자가 효율적으로 상태 관리를 할 수 있도록 도와줍니다.

Redux Toolkit의 주요 기능:

  • 간편한 설정: configureStore를 통해 미들웨어와 리듀서를 손쉽게 설정할 수 있습니다.
  • 보일러플레이트 감소: createSlice와 createAsyncThunk 등을 사용하여 복잡한 액션과 리듀서를 자동으로 생성할 수 있습니다.
  • 내장된 DevTools: Redux DevTools와 통합되어 상태 관리의 디버깅과 추적이 용이합니다.
  • 미들웨어 자동 설정: Thunk와 같은 기본적인 미들웨어가 자동으로 포함되어 있어, 비동기 작업을 손쉽게 처리할 수 있습니다.

 

• Redux와 차이점은 무엇인가요?

Redux  Redux Toolkit
설정이 복잡하고 보일러플레이트 코드가 많음 간단한 설정과 보일러플레이트 코드가 적음
리듀서, 액션 생성자 등을 수동으로 작성해야 함 createSlice, createAsncThunk 로 자동생성
미들웨어 설정이 번거로움 기본적으로 Thunk 미들웨어가 포함됨
오류 처리가 복잡할 수 있음 내장된 툴로 쉽게 디버깅하고 상태를 관리할 수 있음

 

• createSlice (), configureStore()의 역할 

 

 createSlice()는 리듀서와 액션을 한 번에 생성할 수 있는 유틸리티 함수입니다. 상태, 리듀서, 액션 타입을 한 곳에서 관리할 수 있어 코드가 간결해지고 유지보수가 쉬워집니다.

import { createSlice } from '@reduxjs/toolkit';

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

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

 

configureStore()는 Redux 스토어를 설정할 때 사용하는 함수로, 미들웨어와 리듀서를 자동으로 결합하고 DevTools와 통합하는 기능을 제공합니다. 기본적으로 Redux Thunk 미들웨어가 포함되어 있어 비동기 작업 처리가 간편합니다.

import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './counterSlice';

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

export default store;


• Redux Toolkit에서 Thunk 사용법 → createAsncThunk() 란?

createAsyncThunk()는 Redux Toolkit에서 비동기 작업을 처리하기 위해 사용하는 유틸리티 함수입니다.

API 호출 같은 비동기 작업을 간편하게 처리할 수 있으며, 요청의 진행 상태(로딩, 성공, 실패)를 자동으로 관리합니다.

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

// 비동기 작업을 위한 Thunk 생성
export const fetchUserData = createAsyncThunk(
  'user/fetchUserData',
  async (userId, thunkAPI) => {
    const response = await fetch(`https://api.example.com/user/${userId}`);
    return response.json();
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState: { user: null, status: 'idle', error: null },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(fetchUserData.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUserData.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.user = action.payload;
      })
      .addCase(fetchUserData.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  }
});

export default userSlice.reducer;

 

 

  • createAsyncThunk()는 비동기 작업이 진행될 때 각각의 상태(pending, fulfilled, rejected)를 자동으로 처리합니다.
  • extraReducers를 사용하여 비동기 작업의 상태에 따라 다른 동작을 수행할 수 있습니다.

 

 

과제 코드:

 

redux폴더의 redux.js코드:

//import { legacy_createStore, combineReducers } from "redux"
import data from "../assets/data"
import { configureStore, createSlice } from "@reduxjs/toolkit"

export const menuSlice = createSlice({
    name: 'menu',
    initialState: data.menu,
    reducers: {

    }
})

export const cartSlice = createSlice({
    name: 'cart',
    initialState: [],
    reducers: {
        addToCart(state, action) { return [...state, action.payload] },
        removeFromCart(state, action) { return state.filter((el) => action.payload.id !== el.id) },
    }
})

export const store = configureStore({
    reducer: {
        menu: menuSlice.reducer,
        cart: cartSlice.reducer
    }
})


// export const addToCart = (options, quantity, id) => {
//     return {
//         type: 'addToCart',
//         payload: { options, quantity, id }
//     }
// }

// export const removeFromCart = (id) => {
//     return {
//         type: `removeFromCart`,
//         payload: { id }
//     }
// }

// export const cartReducer = (state = [], action) => {
//     switch (action.type) {
//         case 'addToCart': return [...state, action.payload]
//         // action.payload가 { id } 형태이므로, action.payload.id로 접근
//         case 'removeFromCart': return state.filter((el) => action.payload.id !== el.id)
//         default: return state
//     }
// }

// export const menuReducer = (state = data.menu, action) => {
//     return state
// }

//const rootReducer = combineReducers({ cartReducer, menuReducer })

// export const store = legacy_createStore(rootReducer)

 

main.jsx 코드

import ReactDOM from "react-dom/client";
import App from "./App.jsx";
import "./index.css";
import { BrowserRouter } from "react-router-dom";
import { Provider } from "react-redux";
import { store } from "./redux/redux.js"

ReactDOM.createRoot(document.getElementById("root")).render(
  <BrowserRouter>
    <Provider store={store}>
      <App />
    </Provider>
  </BrowserRouter>
);

 

 

Cart.jsx

import { useDispatch, useSelector } from "react-redux";
import data from "../assets/data";
//import { removeFromCart } from "../redux/redux";
import { cartSlice } from '../redux/redux'

function Cart() {

  // const menu = useSelector(state => state.menuReducer)
  // const cart = useSelector(state => state.cartReducer)
  const menu = useSelector(state => state.menu)
  const cart = useSelector(state => state.cart)

  if (!menu)
    return (
      <div style={{ textAlign: "center", margin: "80px" }}>
        {" "}
        메뉴 정보가 없어요!
      </div>
    );
  const allMenus = [...menu.커피, ...menu.논커피];
  return (
    <>
      <h2>장바구니</h2>
      <ul className="cart">
        {cart?.length ? (
          cart.map((el) => (
            <CartItem
              key={el.id}
              item={allMenus.find((menu) => menu.id === el.id)}
              options={el.options}
              quantity={el.quantity}
            />
          ))
        ) : (
          <div className="no-item">장바구니에 담긴 상품이 없어요!</div>
        )}
      </ul>
    </>
  );
}

function CartItem({ item, options, quantity }) {
  const dispatch = useDispatch()
  return (
    <li className="cart-item">
      <div className="cart-item-info">
        <img height={100} src={item.img} />
        <div>{item.name}</div>
      </div>
      <div className="cart-item-option">
        {Object.keys(options).map((el) => (
          <div key={el.id}>
            {el} : {data.options[el][options[el]]}
          </div>
        ))}
        <div>개수 : {quantity}</div>
      </div>
      <button
        className="cart-item-delete"
        onClick={() => {
          dispatch(cartSlice.actions.removeFromCart({ id: item.id }))
          // dispatch(removeFromCart(item.id))
          //setCart(cart.filter((el) => item.id !== el.id));
        }}
      >
        삭제
      </button>
    </li>
  );
}
export default Cart;

 

Menu.jsx

import { useState } from "react";
import Item from "./Item";
import OrderModal from "./OrderModal";
import { useSelector } from "react-redux";

function Menu() {
  const [modalOn, setModalOn] = useState(false);
  const [modalMenu, setModalMenu] = useState(null);

  // const menu = useSelector(state => state.menuReducer)
  const menu = useSelector(state => state.menu)

  if (!menu)
    return (
      <div style={{ textAlign: "center", margin: "80px" }}>
        {" "}
        메뉴 정보가 없어요!
      </div>
    );

  const categorys = Object.keys(menu);
  return (
    <>
      {categorys.map((category) => {
        return (
          <section key={category}>
            <h2>{category}</h2>
            <ul className="menu">
              {menu[category].map((item) => (
                <Item
                  key={item.name}
                  item={item}
                  clickHandler={() => {
                    setModalMenu(item);
                    setModalOn(true);
                  }}
                />
              ))}
            </ul>
          </section>
        );
      })}
      {modalOn ? (
        <OrderModal
          modalMenu={modalMenu}
          setModalOn={setModalOn}
        />
      ) : null}
    </>
  );
}

export default Menu;

 

OrderModal.jsx

import { useState } from 'react'
import data from '../assets/data'
import { useDispatch /*,useSelector*/ } from 'react-redux'
import { /*addToCart,*/ cartSlice } from '../redux/redux'

function OrderModal({ modalMenu, setModalOn }) {
    const [options, setOptions] = useState({ '온도': 0, '진하기': 0, '사이즈': 0 })
    const [quantity, setQuantity] = useState(1)
    // const cart = useSelector(state => state.cartReducer)
    const dispatch = useDispatch()

    const itemOptions = data.options
    console.log(options)
    return (
        <>
            {modalMenu ? (
                <section className="modal-backdrop" onClick={() => setModalOn(false)}>
                    <div className="modal" onClick={(e) => e.stopPropagation()}>
                        <div className='modal-item'>
                            <img src={modalMenu.img} />
                            <div>
                                <h3>{modalMenu.name}</h3>
                                <div>{modalMenu.description}</div>
                            </div>
                        </div>
                        <ul className="options">
                            {Object.keys(itemOptions).map(el => <Option
                                key={el}
                                options={options}
                                setOptions={setOptions}
                                name={el}
                                itemOptions={itemOptions[el]}
                            />)}
                        </ul>
                        <div className="submit">
                            <div>
                                <label htmlFor="count" >개수</label>
                                <input id="count" type="number" value={quantity} min='1' onChange={(event) => setQuantity(Number(event.target.value))} />
                            </div>
                            <button onClick={() => {
                                dispatch(cartSlice.actions.addToCart({ options, quantity, id: modalMenu.id })) // 객체로 만들어서 키 이름이 명확해야한다 id키
                                //dispatch(addToCart(options, quantity, modalMenu.id))
                                //setCart([...cart, { options, quantity, id: modalMenu.id }])
                                setModalOn(false)
                            }}>장바구니 넣기</button>
                        </div>
                    </div>
                </section>
            ) : null}
        </>
    )
}

function Option({ name, options, setOptions, itemOptions }) {
    return (
        <li className='option'>
            {name}
            <ul>
                {itemOptions.map((option, idx) => (
                    <li key={option}>
                        <input type='radio' name={name} checked={options[name] === idx} onChange={() => setOptions({ ...options, [name]: idx })} />
                        {option}
                    </li>
                ))}
            </ul>
        </li>
    )
}

export default OrderModal

 

 

https://github.com/CHOI-JUNWON99/oz-cafe-redux-toolkit/tree/main/src

 

oz-cafe-redux-toolkit/src at main · CHOI-JUNWON99/oz-cafe-redux-toolkit

React 15일차 과제. Contribute to CHOI-JUNWON99/oz-cafe-redux-toolkit development by creating an account on GitHub.

github.com