본문 바로가기

Study/npm Libraries

React-redux & Redux-toolkit

$ npm i redux
$ npm i react-redux

 

  1. redux | 모든 js 프로젝트에서 사용할 수 있는 state management 패키지
  2. react-redux | 리액트 어플리케이션에서 redux의 store/reducer에 간단히 접속할 수 있도록 도와주는 패키지
  3. src/store 폴더를 만들어서 redux와 관련된 파일을 관리한다. 
  4. redux dev tool | redux toolkit을 이용하면 확장 프로그램을 설치만 하면 사용할 수 있다. 아니면 추가적으로 코드를 작성해야 한다. 

redux with react 

컴포넌트에 store 제공하기 | provider 

// src/store/index.js
import { legacy_createStore } from 'redux'
import { INCREMENT } from '../store/index' 
// const redux = require('redux');

// export const INCREMENT = 'increment'; 

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === INCREMENT) {
    return { ...state, counter: state.counter + action.amount };
  }

  if (action.type === 'decrement') {
    return { ...state, counter: state.counter - 1 };
  }
  return state;
};

const store = legacy_createStore(counterReducer);

export default store;

// index.js
import { Provider } from 'react-redux';
import store from './store/index';

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

 

  1. store를 만들고, 제공한다. 
    • 리액트에 의해 가장 먼저 실행되는 index.js 파일에 store를 제공한다. 
    • <Provider> component | provider의 하위 컴포넌트에서 redux에 접근할 수 있다. 
  2. 해당 store를 subscribe하고, reducer에 action을 dispatch하는 것은 컴포넌트에서 작업한다. 
  3. INCREMENT 상수를 만들어서 export하면, 외부 파일에서 action을 dispatch할 때 오타가 생기는 것을 막을 수 있다. 

 

const counterReducer = (state = { counter: 0 }, action) => {
  if (action.type === 'increment') {
    state.counter++; // DON'T DO THIS!!! 
    return state;
  }
  
  /*...*/
}

 

  1. 절대 기존의 state를 변형(mutate)하지 않는다. 예측할 수 없는, 디버깅 하기 어려운 버그를 만들 수 있기 때문이다.
  2. 항상 새로운 객체/배열을 반환해야 한다. immutable data with primitive, reference type 

useSelector | component에서 store에 접근하기

import {useSelector} from 'react-redux';

const Counter = () => {
  const counter = useSelector(state => state.counter);

  return (
    <main>
      <h1>Redux Counter</h1>
      <p>COUNTER VALUE:{counter}</p>
      <button>Toggle Counter</button>
    </main>
  );
};

export default Counter;

 

  • useSelector( (state) => { } ) 
    1. 리액트 리덕스에서 제공하는 커스텀 훅이다. => 리액트 리덕스에 의해 실행된다. 
    2. 리액트 리덕스는 이 훅을 호출하고 있는 컴포넌트를 자동으로 subscriber로 등록한다. 즉, state가 변경되면 subscriber(컴포넌트)를 재호출한다. 
    3. 콜백 함수에서 store에서 관리하고 있는 state 중 일부(part)를 선택할 수 있다. 
    4. 해당 컴포넌트가 unmount 상태가 되면, subscription이 clear 된다. 
    5. +) class component에서는 connect api를 이용한다. 
    6. +) 직접 store 전체에 접근하는 useStore 커스텀 훅도 있다. 

useDispatch | component에서 action을 dispatch 하기

import { useSelector, useDispatch } from 'react-redux';

const Counter = () => {
  const counter = useSelector(state => state.counter);
  const dispatch = useDispatch(); 
  
  const incrementHandler = () => {
    dispatch({type:'increment', amount:5})
  }
  const decrementHandler = () => {
    dispatch({type:'decrement'})
  }

  return (
    <main>
      <h1>Redux Counter</h1>
      <p>COUNTER VALUE:{counter}</p>
      <button onClick={incrementHandler}>increment</button>
      <button onClick={decrementHandler}>decrement</button>
    </main>
  );
};

export default Counter;

 

  • useDispatch 
    1. 아무런 인자도 받지 않고, dispatch를 실행할 수 있는 함수를 반환한다. 
    2. dispatch의 인자로 action을 전달한다. 

redux toolkit  

$ npm i @reduxjs/toolkit

 

react-redux에서 만든 또 다른 패키지로, 리덕스를 이용해서 상태를 관리하는 것을 도와주는 툴이다. toolkit에는 redux 패키지가 포함되어 있다.

 

import { createSlice, configureStore } from '@reduxjs/toolkit'

const initalState = { counter: 0, isVisible: true };
const counterSlice = createSlice({
    name:'counter',
    initialState: initalState, 
    reducers : {
        increment: (state, action)=>{
            // return {...state, counter:state.counter + action.payload}
            state.counter = state.counter + action.payload;
        }
        decrement: (state, action)=>{
            state.counter--;
        }
    }
})

const store = configureStore({
  // reducer: {counter : counterSlice.reducer, auth:...}
  reducer: counterSlice.reducer; 
}); 

export default store;
export const counterActions = counterSlice.actions;

 

  1. configureStore( {reducer: /* ... */} ) | 인자로 reducer라는 프로퍼티를 갖는 객체를 전달해야 한다. 
    • redux 패키지의 create store처럼, store를 만드는 api이다.
    • 이 때 configureStore는 createStore와 달리 여러 개의 reducer를 하나의 reducer로 합칠 수 있다(merge). 
      • 최종적으로 redux는 항상 global state를 관리하는 단 하나의 reducer 만을 갖는다. 
    • slice가 여러 조각일 때, store에 연결해야 할 reducer도 많아지기 때문에 유용하게 사용할 수 있다. 
    • +) redux에도 위와 같은 역할을 하는 combineReducers라는 api가 있다.
  2. createSlice | 특정 상태를 관리하는 조각을 만들 수 있다. 
    • 예를 들어 counter, authorization과 관련된 state를 각각 분리하는 조각을 만들 수 있다. 
    • name, initialState, reducers라는 프로퍼티를 갖는다.  
    • create slice는 모든 reducer에 자동으로 고유한 action을 할당한다. => 개발자는 따로 action을 지정할 필요가 없다. 
  3. state immutability 
    • 리덕스 toolkit은 내부적으로 immer라는 패키지를 사용하고 있다.
    • 이 패키지는 reducer 함수 내에서 state를 직접 변형하는 코드(ex. state.count++)를 변환해서, 실제로는 state immutability를 유지할 수 있게 해준다.

 

import { useDispatch, useSelector, } from 'react-redux';
import { counterActions } from '../store/index';

const Counter = () => {
 // 여러 개의 slice를 사용하고 있을 때
 // const isVisible = useSelector(state => state.counter.isVisible);
 const dispatch = useDispatch(); 
 const incrementHandler = () => {
   dispatch(counterActions.increment(5)); 
 } 
}

 

  1. conterActions = counterSlice.actions; 
  2. reducer 메서드의 이름을 이용하면, ex) counterSlice.actions.increment(), 자동으로 action 객체가 생성되고 그 값이 dispatch의 인자로 할당된다. 
  3. action 객체는 다음과 같이 구성되어있고, increment(5)의 5는 payload 프로퍼티의 값으로 전달된다. 
    • { type: unique identifier, payload: additional data...}
    • increment: (state, action) => {  state.count = state.count + action.payload; }

async code with redux 

  1. reducer function | 일반적인 개념으로서 reducer는 다음과 같은 조건을 만족해야 한다. 
    • 순수함수여야 한다. | why reducer should be a pure function? 
      • 리덕스는 old state를 각 reducer 함수에 input으로 전달하고, 각 reducer 함수가 새로운 state를 반환할 것을 기대한다.
      • 이 때 새로운 state와 old state를 비교해서, 변화가 없다면 기존의 state를, 어떤 변화가 있다면 새로운 state를 반환한다.
      • 이 때 새로운 state와 old state 객체가 저장되어있는 메모리 주소를 비교하기 때문에, reducer 함수가 input을 mutate하지 않는 순수함수 여야 하는 것이다. => 실제 어플리케이션에서 deep comparison은 highly cost task이기 때문에 사용되지 않는다. 
    • ??? 코드 내부에 side effect가 없어야 한다.
      • side effect 코드는 unpredictable 하기 때문에...? 순수함수와 달리 무언가를 바꾸기 때문에??? 
      • ex) http request, read local-storage... 
  2. side effect 코드는 reducer 함수 밖에서 작성되어야 한다.
    • useEffect | 컴포넌트에서 useEffect 실행
    • action creator fn | redux toolkit이 자동으로 만드는 action 대신, action을 커스텀해서 사용한다. 

useEffect 

let isInitial = true; 

function App() {
  const orderList = useSelector((state) => state.cart.orderList);
  const notification = useSelector((state) => state.ui.notification);
  const dispatch = useDispatch();


  useEffect(() => {
    const sendData = async() => {
    // try ... 
      const res = await fetch(
        'https://react-http-1de5e-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json',
        {
          method: 'PUT',
          body: JSON.stringify({ orderList }),
        }
      );
      
      if (!res.ok) throw new Error('sending cart data failed');

      dispatch(
        uiActions.showNotification({
          status: 'success',
          title: 'Success',
          message: 'sent your cart data!',
        })
      );
      
    // catch ...
    }
    
    if(isInitial) {
      isInitial = false; 
      return ;
    }
    
    sendData(); 
  }, [orderList, dispatch]);

  return (
    /*...*/
  );
}

 

  1. useSelector | 컴포넌트가 state를 subscribe한다. => orderList가 변경될 때 마다 app 컴포넌트가 재평가된다. 
  2. useEffect | orderList state가 업데이트 될 때 마다 콜백함수를 실행한다. 
    • useEffect는 dependency가 변할 때 뿐만 아니라, app이 시작 될 때에도 실행된다. => isInitial
    • 즉, orderList가 텅 비어있는 초기 상태에서도 백엔드에 요청을 전송한다.
  3. isInitial 
    • 컴포넌트 밖에서 isInitial을 정의 => 앱이 렌더링 될 때 마다 해당 값이 재평가되지 않도록 한다. 
    • app이 처음 렌더링 될 때 sendData를 실행하지 않도록 한다. 
  4. PUT | 백엔드가 처리하는 로직으로, 기존의 특정 데이터를 수정한다(override).  
  5. 컴포넌트가 무거워진다. 

action creator & thunk

thunk

 

  1. thunk | a function that delays an action untill later
    • 어떤 일이 끝날 때 까지 action을 반환하는 것을 미루는 함수.
  2. action creator | action을 만든다. 
    • ex) uiSlice.actions.showNotification({ status:...}) => redux tool kit이 자동으로 action을 만들어 반환해 준다.
  3. action creator를 thunk로 작성한다.
    • action을 반환하는 것을 미룰 수 있다. 
    • 대신 action creator는 action을 직접 반환하는 것이 아니라, action을 반환하는 함수를 반환하게 된다.
    • 즉 action을 dispatch 하기 전에, 다른 코드를 실행할 수 있다. 
    • 이 때 action creator는 slice 외부에 작성한다. 
  4. 컴포넌트를 가볍게(lean) 유지할 수 있다. 

 

 // app.js
 useEffect(() => {
  if (isInit) {
    isInit = false;
    return;
  }
  
  dispatch(sendCartData(orderList));
}, [orderList, dispatch]);

// store/cart.js
const sendCartData = (cartData) => {
  return async(dispatch)=>{
    // do some async tasks
    
    dispatch(/*some actions with cartData*/)
  }
}

 

  1. dispatch의 인자로 전달한 sendCart는 action creator 함수이다. 
  2. action creator는 action을 반환하는 함수를 반환하는데, 이 함수에서 dispatch를 이용해 action을 반환한다. 

 

// store/cart.js
const cartInitState = {}

const cartSlice = {
  name:'cart'
  initialState: cartInitState,
  reducers: {
    addItem:()=>{}, 
    removeItem:()=>{}, 
  }
}

export default cartSlice;
export const cartActions = cartSlice.actions;

export const sendCartData = (cartData) => {
  return async (dispatch) => {
    try {
      dispatch(
        uiActions.showNotification({
          status: 'pending',
          title: 'Pending',
          message: 'sending your cart data...',
        })
      );

      const res = await fetch(
        'https://react-http-1de5e-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json',
        {
          method: 'PUT',
          body: JSON.stringify({ cartData }),
        }
      );

      if (!res.ok) throw new Error('sending cart data failed');

      dispatch(
        uiActions.showNotification({
          status: 'success',
          title: 'Success',
          message: 'sent your cart data!',
        })
      );
    } catch (error) {
      dispatch(
        uiActions.showNotification({
          status: 'error',
          title: 'Error',
          message: `failed to send cart data`,
        })
      );
    }
  };
};

 

'Study > npm Libraries' 카테고리의 다른 글

React with web accessibility  (0) 2022.09.19
TanStack Query(React Query)  (0) 2022.07.24
React-router(v6)  (0) 2022.07.03
React-router  (0) 2022.07.01
what is Redux?  (0) 2022.06.29