본문 바로가기

React

[React] Redux

리덕스

정의

크로스 컴포넌트 또는 앱 와이드 상태를 위한 상태 관리 시스템

애플리케이션에 있는 하나의 중앙 데이터(상태) 저장소 - 하나에 모든 데이터 상태 저장

 

***

로컬 상태 : 하나의 컴포넌트에 연관된 상태값 

크로스 컴포넌트 상태 :  다수의 컴포넌트에 미치는 상태값 (ex) Modal 여닫는 상태값, prop drilling prop chains 로 구현)

앱와이드상태 : 전체 앱에 영향을 주는 상태 ex) 사용자 인증, prop drilling prop chains 로 구현)

규칙

순수 함수

동기식 이어야 하며 부수 효과가 없어야 함(리듀서 안에서 부수효과 사용 불가)

 

리덕스 VS 리액트 컨텍스트

리액트 컨텍스트 단점

- 설정 복잡, 관리 복잡, 대형 애플리케이션에서 구축할 경우 중첩된 JSX 코드가 나올 수 있음

- 성능 : 테마를 변경하거나 인증 같은 저빈도 업데이트에는 좋음, 데이터가 자주 변경되는 경우는 적합하지 않음

 

리덕스 작동방식

상태를 관리하는 중앙 데이터 저장소에서 Component를 구독. 

데이터 변경할 때마다 저장소가 컴포넌트에게 알려줌 

컴포넌트는 필요한 데이터 받음

컴포넌트는 저장소에 있는 데이터를 직접 조작하지 않음 > Reducer 함수를 통해 데이터를 변경

컴포넌트가 액션ex) 버튼 클릭 을 취하면 액션이 발송됨(객체로 발송) > 리덕스는 액션을 리듀서로 전달 > 리듀서가 수행하게 됨

 

리듀서 

중앙 데이터 저장소의 상태를 바꾸기 위한 함수

리듀서는 항상 새로운 상태 객체를 return 해야함

Inputs: old state + dispatched action

output : 새 상태 객체

부수적인 effect가 없어야 함 : http 요청 전송, 로컬 저장소에 기록, 로컬 저장소로부터 가져오는것 하면 안됨

 

사용방법

index.js

import React from "react";
import ReactDOM from "react-dom/client";
import { Provider } from "react-redux";

import "./index.css";
import App from "./App";
import store from "./store/index.js"; 

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

 

   <Provider> : 최상단에서 데이터 저장소 제공

 

 

Counter.js

import { useSelector, useDispatch } from "react-redux";
import classes from "./Counter.module.css";

import { counterActions } from "../store/counter.js";

//import { Component } from "react";
// import {  connect } from "react-redux";

const Counter = () => {
  const dispatch = useDispatch();
  // state.counter은 slice.reducer에 접근
  // state.counter.counter은 slice의 리듀서가 만든 state에 접근
  const counter = useSelector((state) => state.counter.counter);
  const show = useSelector((state) => state.counter.showCounter);

  const toggleCounterHandler = () => {
    dispatch(counterActions.toggleCounter());
  };
  // 액션 객체가 자동으로 생성됨
  const incrementHandler = () => {
  
    dispatch(counterActions.increment());
  };

  const increaseHandler = () => {
    dispatch(counterActions.increase(5));
  };

  const decrementHandler = () => {
    dispatch(counterActions.decrement());
  };

  return (
    <main className={classes.counter}>
      <h1>Redux Counter</h1>
      {show && <div className={classes.value}>{counter}</div>}
      <div>
        <button onClick={incrementHandler}>Increment</button>
        <button onClick={increaseHandler}>Increase by 5</button>
        <button onClick={decrementHandler}>Decrement</button>
      </div>
      <button onClick={toggleCounterHandler}>Toggle Counter</button>
    </main>
  );
};

export default Counter;

 

useSelector 

저장소에 직접 접근 가능, 

useSelector를 사용하면 react-redux가 자동으로 이 컴포넌트를 위해 리덕스 저장소에 자동 구독 설정

저장소 값이 바뀌면 자동으로 업데이트 되고 최신 카운터 받음

컴포넌트가 사라지면 자동으로 해지 해줌

 

useDispatch

저장소에 대한 action을 보냄

 

store/counter.js

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



//초기값
const initialCounterState = { counter: 0, showCounter: true };

const counterSlice = createSlice({
  name: "counter",
  initialState: initialCounterState,
  reducers: {
  // state : 최신 상태
  // action : 컴포넌트가 보낸 액션
    increment(state) {
      state.counter++;
    },
    decrement(state) {
      state.counter--;
    },
    increase(state, action) {
      state.counter = state.counter + action.payload;
    },
    toggleCounter(state) {
      state.showCounter = !state.showCounter;
    },
  },
});

//slice에서 설정한 리듀서에 접근 가능
export default counterSlice.reducer;
export const counterActions = counterSlice.actions;

 

createSlice

- redux-toolkit에서 사용하는 reducer 함수

- reducers : 나중에 리덕스에 의해 호출 됨

 

 

 

store/index.js

import { createSlice, configureStore } from "@reduxjs/toolkit";
import authReducer from "./auth";
import counterReducer from "./counter";

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

export default store;

configureStore

- 여러개의 리듀서를 하나의 리듀서로 합칠 수 있음

- configureStore({}) :리듀서 프로퍼티를 정하는 설정 객체 

 

비동기식(ex, HTTP 요청), 부수 효과 사용시 

- useEffect 사용

App.jsx

import { Fragment, useEffect } from "react";
import Cart from "./components/Cart/Cart";
import Layout from "./components/Layout/Layout";
import Products from "./components/Shop/Products";
import { useSelector, useDispatch } from "react-redux";
import { sendCartData, fetchCartData } from "./store/cart-actions";
import Notification from "./components/UI/Notification";

let isInitial = true;

function App() {
  const showCart = useSelector((state) => state.ui.cartIsVisible);

  const cart = useSelector((state) => state.cart);

  const notification = useSelector((state) => state.ui.notification);
  const dispatch = useDispatch();

  useEffect(() => {
    dispatch(fetchCartData());
  }, [dispatch]);
  
  //useSelector는 리덕스가 컴포넌트를 구독하게 만들기 때문에
  // useEffect의존성에 넣은 cart 데이터 상태가 바뀌면 컴포넌트가 알아챔
  // 이로인해 컴포넌트가 재 실행됨
  useEffect(() => {
    if (isInitial) {
      isInitial = false;
      return;
    }

    if (cart.changed) {
      dispatch(sendCartData(cart));
    }
  }, [cart, dispatch]);

  return (
    <Fragment>
      {notification && (
        <Notification
          status={notification.status}
          title={notification.title}
          message={notification.message}
        />
      )}
      <Layout>
        {showCart && <Cart />}
        <Products />
      </Layout>
    </Fragment>
  );
}

export default App;

 

- action & creator 사용

Thunk : 다른 작업이 완료될 때까지 작업을 지연

// 다른 함수를 반환하는 함수 작성
// 이 함수는 독립적인 js 함수로 리듀서에 도달하지 않았기에 비동기식 코드 작성 가능
//리덕스는 함수를 반환하는 함수도 허용하기 떄문에, 자동으로 디스패치 인수를 줌 
export const sendCartData = (cart) => {
  return async (dispatch) => {
    dispatch(
      uiActions.showNotification({
        status: "pending",
        title: "Sending...",
        message: "Sending cart data!",
      })
    );

    const sendRequest = async () => {
      const response = await fetch(
        "https://주소..",
        {
          method: "PUT",
          body: JSON.stringify({
            items: cart.items,
            totalQuantity: cart.totalQuantity,
          }),
        }
      );

      if (!response.ok) {
        throw new Error("Sending cart data failed");
      }
    };

    try {
      await sendRequest();

      dispatch(
        uiActions.showNotification({
          status: "success",
          title: "Success...",
          message: "Sent cart data successfully",
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: "error",
          title: "Error...",
          message: "Sent cart data failed",
        })
      );
    }
  };
};

 

 

 

 

 

 

 

 

'React' 카테고리의 다른 글

[React] router loader, useNavigation  (0) 2024.04.24
[React] router  (0) 2024.04.23
[React] Form Validation check  (0) 2024.04.18
[React] Custom Hook  (0) 2024.04.17
[React] Http 연결, fetch  (0) 2024.04.16