Archive

[Redux] Redux & Redux-toolkit

manon_e 2022. 2. 13. 17:53
반응형

 

 

 

     


     

    What is "Redux" ?

     

     

    🔻 A state management system for cross-component or app-wide state.

     

     

    ✔️  state를 3종류로 크게 나눌 수 있다.

    1. Local state : 하나의 컴포넌트에 종속되는 state

    2. Cross-Component state : 하나 이상의 컴포넌트에 영향을 미치는 state(prop drilling)

    3. App-Wide state : app전체의 기본적인 모든 컴포넌트에 영향을 미치는 state 

     

     

    🤷🏻‍♀️   React Context도 있는데 왜 Redux? context API의 단점

     > 대규모의 app작업 시 state관리를 위해

          : 수많은 context provider 컴포넌트가 중첩되어 복잡해진다.

          : provider개수를 줄이고 하나의 거대한 context provider로는 관리가 불가능

     > state 변동 횟수가 높은 경우에 성능문제가 있다.

     

     

     

     

     

     


     

     

     

     

     

    Redux 기본 작동 방식

     

     

     

    Redux는 app에 하나의 중앙 데이터(state) 저장소를 가진다.  ( app전체를 위한 하나의 store! )

     

    전역상태를 하나의 저장소에 저장하며, action(어떤 상태변경이 생길지 서술하는 객체)을 내보내서(dispatch) 상태변경을 한다.

    그리고 action이 전체 application의 상태를 어떻게 변경할지 명시하기 위해서 reducer의 작성이 필요하다.

     

     

     

     

    🔻 Redux 흐름

    1. component가 특정 action을 trigger ( action은 단순 자바스크립트 객체 - reducer가 수행하는 작업의 종류를 설명 )

    2. actions을 reducer로 전달하여 원하는 작업 호출 

    3. reducer는 중앙데이터 store에 존재하는 데이터를 교체하는 새로운 state를 보냄

    4. 데이터 store에 있는 state가 update

    5. subscribe하는 component에 알려져 UI update

     

     

     

     

     

    ✔️  Reducer function

      항상 두개의 매개변수 (기존상태와 dispatched action)를 입력받아서 새로운 상태를 리턴해야한다.

      리듀서 함수는 순수함수이다. (입력과 value가 같다면 항상 같은 출력을 생산한다.)

     

     

     

    const redux = require("redux");
    
    //reducer function
    const counterReducer = (state = { counter: 0 }, action) => {
      if (action.type === "increment") {
        return {
          counter: state.counter + 1,
        };
      }
      if (action.type === "decrement") {
        return {
          counter: state.counter - 1,
        };
      }
    };
    
    //central Data Store
    const store = redux.createStore(counterReducer);
    
    //Subscription
    const counterSubscriber = () => {
      const latestSate = store.getState();
      console.log(latestSate);
    };
    
    store.subscribe(counterSubscriber);
    
    store.dispatch({ type: "increment" });
    store.dispatch({ type: "decrement" });
    
    //출력
    { counter : 1 }
    { counter : 0 }

     

     

     

     

     


     

     

     

     

     

    Redux 사용

     

     

     

    1. store생성 

    store / index.js

    import { createStore } from "redux";
    
    const counterReducer = (state = { counter: 0 }, action) => {
      if (action.type === "increment") {
        return {
          counter: state.counter + 1,
        };
      }
      if (action.type === "decrement") {
        return {
          counter: state.counter - 1,
        };
      }
      return state;
    };
    
    const store = createStore(counterReducer);
    
    export default store;

     

     

     

    2. 가장 높은 component 레벨에서 store 제공

    provider로 감싸주고 위에서 store를 props로 넘겨준다.

    Index.js
    
    //import React from "react";//
    //import ReactDOM from "react-dom";
    import { Provider } from "react-redux";
    //import App from "./App";
    import store from "./store/index";
    
    ReactDOM.render(
      <Provider store={store}>
        <App />
      </Provider>,
      document.getElementById("root")
    );

     

     

    3. react component에서 redux data 사용

        useSelector hook으로 store에서 state가져오고, 

        useDispatch hook으로 redux store에 대한 action을 보낸다.

    import { useSelector, useDispatch } from "react-redux";
    
    const Counter = () => {
      const dispatch = useDispatch();
      const counter = useSelector((state) => state.counter);
    
      const incerementHandler = () => {
        dispatch({ type: "increment" });
      };
      const decerementHandler = () => {
        dispatch({ type: "decrement" });
      };
    
      return (
        <main className={classes.counter}>
          ...
          <div>
            <button onClick={incerementHandler}>Increment</button>
            <button onClick={decerementHandler}>Decrement</button>
          </div>
          ...
        </main>
      );
    };
    
    export default Counter;

     

     

    📌  payload 

         action에 속성 추가하기

         (reducer에서 하드코딩 하지않고 action으로부터 값을 얻어서 데이터를 바꾼다.)

     

      if (action.type === "increase") {
        return {
          counter: state.counter + action.amount,
        };
      }
    const Counter = () => {
      const dispatch = useDispatch();
      const counter = useSelector((state) => state.counter);
    
    
      const increseHandler = () => {
        dispatch({ type: "increase", amount: 10 });
      };
    
    
      return (
        <main className={classes.counter}>
             ...
            <button onClick={increseHandler}>Increse</button>
             ...
        </main>
      );

     

     

     

     

    💥   여러 state 속성 작업

          : Reducer에서 state 추가

     

    > store에서 기존 counter에서 showCounter추가

    //store-index.js
    const initialState = { counter: 0, showCounter: true };
    const counterReducer = (state = initialState, action) => {
      if (action.type === "toggle") {
        return {
          showCounter: !state.showCounter,
          counter: state.counter,
        };
      }
      ...

     

    const Counter = () => {
      const dispatch = useDispatch();
      const counter = useSelector((state) => state.counter);
      const show = useSelector((state) => state.showCounter);
    
      const toggleCounterHandler = () => {
        dispatch({ type: "toggle" });
      };
    
      return (
        <main className={classes.counter}>
          {show && <div className={classes.value}>{counter}</div>}
           ...
           <button onClick={toggleCounterHandler}>Toggle Counter</button>
        </main>
      );
    };

     

     

     

     

     

     


     

     

     

     

     

     

    Redux toolkit 

     

     

     

    🔻 redux를 좀 더 쉽게 사용하기 위하여 나온 도구모음!

        redux로직을 작성하는 표준 방식이 되기 위한 의도로 만들어 졌다.

        핵심은 기존 리덕스의 복잡함을 낮추고 사용성을 높이는 것이다.

     

        

     

    리덕스의 대표적인 문제

    - store 환경설정이 복잡하다.

    - 유용하게 사용하려면 많은 패키지를 추가해야 한다.

    - 보일러플레이트 (어떤 일을 하기 위해 꼭 작성해야 하는 코드)를 많이 요구한다.

     

     

     

    ✔️ toolkit에 redux가 이미 포함되어있어서, toolkt을 사용하려면 기존에 설치된 redux를 삭제해준다.

    npm i @reduxjs/toolkit

     

     

    https://redux-toolkit.js.org/

     

    Redux Toolkit | Redux Toolkit

    The official, opinionated, batteries-included toolset for efficient Redux development

    redux-toolkit.js.org

     

     

     

     

     

     


     

     

     

     

     

     

    Redux toolkit 사용

     

     

     

     

    1.  createSlice

          리덕스 로직 작성 시 ducks pattern 형태로 작성을 돕는 createSlice를 사용한다.

         특징

          - 객체를 인자로서 생성.

          - 유지보수하기 쉬움.

          - 따로 상태를 복사하지않고 바로 변경가능

           (toolkit는 내부적으로 immer라는 패키지를 사용하여 코드를 감지하고 자동으로 원래 있는 상태를 복제)

          - 상태와함께 action도 받음 (action.payload)

    import { createSlice } from "@reduxjs/toolkit";
    const initialCounterState = { counter: 0, showCounter: true };
    
    const counterSlice = createSlice({
      name: "counter",   //모든 slice는 name가져야함(상태식별자)
      initialState: initialCounterState,  //초기값설정
      reducers: {
        //기존상태를 바꾸는 것이아님, 자동으로 기존의 상태를 복제해서 변형시킴 (불변성 신경쓸 필요없어짐)
        increment(state) {
          state.counter++;
        },
        decrement(state) {
          state.counter--;
        },
        increase(state, action) {
          state.counter = state.counter + action.payload;
        },
        toggleCounter(state) {
          state.showCounter = !state.showCounter;
        },
      },
    });

     

     

     

     

     

    2.  configureStore

        : 스토어를 구성하는 함수이며,  여러개의 reducer를 하나의 reducer로 쉽게 합칠 수 있다.

          여러 slice를 내보낼 때는 하나의 객체로 묶어서 key값을 설정하여 보낸다.

          (기존 reduxdml combineReducers 함수와 같은역할)

     

          리덕스 코어 라이브러리의 표준 함수인 createStore를 추상화한 것이다.

          (기존 리덕스의 번거로운 기본 설정과정을 자동화)

    import { createSlice, configureStore } from "@reduxjs/toolkit";
    
    const counterSlice = createSlice({
      ...
    )}
    
    const store = configureStore({
      reducer: counterSlice.reducer,
    });
    
    export const counterActions = counterSlice.actions;

    slicename.actions.keyname = actions 생성자

    ( ex. counterSlice.actions.increment > 액션전달 )

     

     

     

     

     

    3. state를 사용할 component에서 import 

     

    import { counterActions } from "../store/index";
    import { useSelector, useDispatch } from "react-redux";
    import { counterActions } from "../store/index";
    
    const Counter = () => {
      const dispatch = useDispatch();
      const counter = useSelector((state) => state.counter);
      const show = useSelector((state) => state.showCounter);
      //store에서 key값 설정을 하지않았다면 state.counter
      //key값 설정을 했으면 key값을 추가해서 state.key.counter
    
      const incerementHandler = () => {
        dispatch(counterActions.increment());
      };
      const decerementHandler = () => {
        dispatch(counterActions.decrement());
      };
      const increseHandler = () => {
        //{type:SOME_UNIQUE+IDENTIFIER, payload : 10}
        //payload는 toolkit가 기본값으로 사용하는 필드명
        dispatch(counterActions.increase(10));
      };
      const toggleCounterHandler = () => {
        dispatch(counterActions.toggleCounter());
      };

     

    payload가 필요할 시 값을 괄호에 전달해 주고, reducer에서 action.payload로 접근한다.

    ( toolkit는 자동으로 action 생성자를 생성해서 redux toolkit가 생성한 type: SOME_UNIQUE_IDENTIFIER을 전달하고,

      인자로서 실행하고자 하는 액션 메서드에 전달한 값을 추가 필드명이 payload인 곳에 저장한다.)

     

     

     

     

     

     

    4. 여러 slice관리

        위 과정과 동일하게 slice를 추가하고 store에 추가만 해주면 된다.

        ( slice가 여러개라도 redux store는 하나밖이어서 configureStore를 한번만 호출해야한다! )

     

    const AuthSlice = createSlice({
      name: "authentication",
      initialState: { isAuthenticated: false },
      reducers: {
        login(state) {
          state.isAuthenticated = true;
        },
        logout(state) {
          state.isAuthenticated = false;
        },
      },
    });
    
    const store = configureStore({
      reducer: { counter: counterSlice.reducer, auth: AuthSlice.reducer },
    });
    
    export const authActions = authSlice.actions;

     

     

     

    더보기
    //store/index.js
    // import { createStore } from "redux";
    //slice는 createReducer보다 강력하고 한번에 몇가지를 단순화한다.
    import { createSlice, configureStore } from "@reduxjs/toolkit";
    
    const initialCounterState = { counter: 0, showCounter: true };
    
    const counterSlice = createSlice({
      //전역상태의 slice를 미리만든다
      name: "counter",
      initialState: initialCounterState, //초기값설정
      reducers: {
        //기존상태를 바꾸는 것이아님, 자동으로 기존의 상태를 복제해서 변형시킴 (불변성 신경쓸 필요없어짐)
        increment(state) {
          state.counter++;
        },
        decrement(state) {
          state.counter--;
        },
        increase(state, action) {
          state.counter = state.counter + action.payload;
        },
        toggleCounter(state) {
          state.showCounter = !state.showCounter;
        },
      },
    });
    
    const authSlice = createSlice({
      name: "authentication",
      initialState: { isAuthenticated: false },
      reducers: {
        login(state) {
          state.isAuthenticated = true;
        },
        logout(state) {
          state.isAuthenticated = false;
        },
      },
    });
    // const counterReducer = (state = initialState, action) => {
    //   if (action.type === "increment") {
    //     return {
    //       counter: state.counter + 1,
    //       showCounter: state.showCounter,
    //     };
    //   }
    //   if (action.type === "decrement") {
    //     return {
    //       counter: state.counter - 1,
    //       showCounter: state.showCounter,
    //     };
    //   }
    
    //   if (action.type === "increase") {
    //     return {
    //       counter: state.counter + action.amount,
    //       showCounter: state.showCounter,
    //     };
    //   }
    
    //   if (action.type === "toggle") {
    //     return {
    //       showCounter: !state.showCounter,
    //       counter: state.counter,
    //     };
    //   }
    //   return state;
    // };
    
    const store = configureStore({
      reducer: { counter: counterSlice.reducer, auth: authSlice.reducer },
    });
    // configureStore로 여러 slice내보낼 수 있다.
    // 여러개 내보낼때는 하나의 객체로 묶어서 key값을 설정하여서 보낸다.
    // reducer : {counter : counterSlice.reduce}
    export const counterActions = counterSlice.actions;
    export const authActions = authSlice.actions;
    
    export default store;

     

    //Counter.js
    import classes from "./Counter.module.css";
    import { useSelector, useDispatch } from "react-redux";
    import { counterActions } from "../store/index";
    
    const Counter = () => {
      const dispatch = useDispatch();
      const counter = useSelector((state) => state.counter.counter);
      const show = useSelector((state) => state.counter.showCounter);
    
      const incerementHandler = () => {
        dispatch(counterActions.increment());
      };
      const decerementHandler = () => {
        dispatch(counterActions.decrement());
      };
      const increseHandler = () => {
        //{type:SOME_UNIQUE+IDENTIFIER, payload : 10}
        //payload는 toolkit가 기본값으로 사용하는 필드명
        dispatch(counterActions.increase(10));
      };
      const toggleCounterHandler = () => {
        dispatch(counterActions.toggleCounter());
      };
    
      return (
        <main className={classes.counter}>
          <h1>Redux Counter</h1>
          {show && <div className={classes.value}>{counter}</div>}
          <div>
            <button onClick={incerementHandler}>Increment</button>
            <button onClick={decerementHandler}>Decrement</button>
            <button onClick={increseHandler}>Increse</button>
          </div>
          <button onClick={toggleCounterHandler}>Toggle Counter</button>
        </main>
      );
    };
    
    export default Counter;
    
    // class Counter extends Component {
    //   incerementHandler() {
    //     this.props.increment();
    //   }
    //   decerementHandler() {
    //     this.props.decrement();
    //   }
    //   toggleCounterHandler() {}
    //   render() {
    //     return (
    //       <main className={classes.counter}>
    //         <h1>Redux Counter</h1>
    //         <div className={classes.value}>{this.props.counter}</div>
    //         <div>
    //           <button onClick={this.incerementHandler.bind(this)}>Increment</button>
    //           <button onClick={this.decerementHandler.bind(this)}>Decrement</button>
    //         </div>
    //         <button onClick={this.toggleCounterHandler}>Toggle Counter</button>
    //       </main>
    //     );
    //   }
    // }
    // const mapStateToProps = (state) => {
    //   return {
    //     counter: state.counter,
    //   };
    // };
    
    // const mapDispatchToProps = (dispatch) => {
    //   return {
    //     increment: () => dispatch({ type: "increment" }),
    //     decrement: () => dispatch({ type: "decrement" }),
    //   };
    // };
    // export default connect(mapStateToProps, mapDispatchToProps)(Counter);

     

    반응형

    'Archive' 카테고리의 다른 글

    [TIL220220] MongoDB 사용  (0) 2022.02.21
    [Next.JS] Dynamic Routes  (0) 2022.02.15
    [TIL220128] Image uploader  (0) 2022.01.28
    [TIL220127] localStorage  (0) 2022.01.28
    [React] React 동작원리 ( + useMemo / useCallback )  (0) 2022.01.27