[리액트] 전역상태관리 라이브러리 Redux

iberis2·2023년 2월 24일
0

React 리액트

목록 보기
4/20

Redux

전역 상태관리

리액트에서 여러 컴포넌츠에서 사용되는 값이 변하는 데이터(즉, state = 상태)는 해당 컴포넌츠들의 공통된 상위 컴포넌츠에 정의한다.
그리고 해당 상태를 필요로 하는 컴포넌츠로 전달하고 → 전달해서 → 사용하게 된다.

그런데 모든 상태들을 이렇게 관리하는 것에는 문제가 있다.

  • items 를 직접 사용하지 않는 App 컴포넌츠, ItemListContainer, ShoppingCart도 상태 데이터를 가짐
  • 상태 끌어올리기, Props 내려주기를 여러 번 거쳐야 함
    Props Driling (상위 컴포넌트의 state를 props를 통해 전달하고자 하는 컴포넌트로 전달하기 위해 그 사이는 props를 전달하는 용도로만 쓰이는 컴포넌트들을 거치면서 데이터를 전달하는 현상)이 발생한다.
  • 애플리케이션이 복잡해질수록 데이터 흐름도 복잡해짐
  • 컴포넌트 구조가 바뀐다면, 지금의 데이터 흐름을 완전히 바꿔야 할 수도 있음


이러한 단점을 보완하기 위한 방법으로는
① 컴포넌트와 관련있는 state는 될 수 있으면 가까이 유지하는 방법과
② 상태관리 라이브러리를 사용하는 방법이 있다.
상태관리 라이브러리를 사용하게 되면 전역으로 관리하는 저장소에서 직접 state를 꺼내쓸 수 있다.

🗂 React에서 사용할 수 있는 대표적인 상태관리 라이브러리로 Redux가 있다.

[주의] 💡 Redux는 React의 관련 라이브러리, 혹은 하위 라이브러리가 아니며, React 없이도 사용할 수 있는 상태 관리 라이브러리이다.

React에서 Redux 사용하기

개념

  • Reducer : state를 업데이트 하는 역할
  • Dispatch : state 업데이트를 위한 요구
  • Action : 요구의 내용
  • Store : 상태가 관리되는 오직 하나뿐인 저장소

Dispatch(Action) → Reducer(State, Action) → Store에 있는 State를 변화시킴
Dispatch() 함수에 Action을 넣어서 Reducer 에게 전달 하면 컴포넌츠의 state를 지정한 Action 대로 업데이트하게 된다.

Redux에서는 Action → Dispatch → Reducer → Store 순서로 데이터가 단방향으로 흐른다.

설치

리액트 프로젝트의 터미널에서 npm install react-redux 또는 yarn add react-redux 로 설치한다.

Store 저장소 만들기

// store 폴더 안의 store.js

import { createStore } from 'redux';
import rootReducer from '../reducers/index';

const store = createStore(rootReducer);

export default store;

Store를 생성할 파일에서 createStore 메서드를 활용해 Reducer를 연결해서 Store를 생성할 수 있다.

// index.js 
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import store from './store/store';
import { Provider } from 'react-redux';

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

전역 상태 저장소 store를 사용하기 위해서는 최상위 컴포넌트인 App 컴포넌트를 Provider로 감싸준 후 props로 변수 store를 전달해주여야 한다.

Action 객체

state를 어떻게 변화시킬 지 정의해 놓은 객체로 dispatch( )의 파라미터로 전달해서 사용한다.
dispatch({ Action 객체 })

// type을 요청받았을 때 어떤 값을 반환할 지 payload가 필요 없는 경우
{type: "ADD_TO_CART" } // type 은 필수로 지정해야한다.

// payload가 필요한 경우
{ type: "ADD_TO_CART", payload: {quantity: 1, itemId} }
// ADD_TO_CART 라는 요청이 들어오면 {quantity: 1, itemId} 객체로 state를 바꾸기 위해 payload를 지정함 

Action 객체가 어떤 동작을 하는지 명시하는 type 은 필수로 지정을 해 주어야 한다.

  • 대문자와 Snake Case로 작성합니다.

필요에 따라 payload 프로퍼티를 작성해 구체적인 값을 전달한다.

// 직접 객체를 만들기 보다는 Action Creator 함수로 Action 객체를 작성한다.
const addToCart = (itemId) => {
  return {
    type: "ADD_TO_CART",
    payload: {
      quantity: 1,
      itemId,
    },
  };
};

보통 Action을 직접 작성하기보다는 Action 객체를 생성하는 함수를 만들어 사용하는 경우가 많다. 이러한 함수를 액션 생성자(Action Creator)라고도 한다.

Dispatch 전달자

Dispatch는 Reducer로 Action을 전달해주는 함수이다.

  • useDispatch( )로 생성한다.
  • Dispatch의 전달인자로 Action 객체(또는 Action 생성자 함수)가 전달된다.
import { useDispatch } from 'react-redux'

const dispatch = useDispatch()
dispatch({type: "ADD_TO_CART" })

useDispatch()

useDispatch() 는 Action 객체를 Reducer로 전달해 주는 Dispatch 함수를 반환하는 메서드이다.

useSelector()

useSelector()는 컴포넌트와 state를 연결하여 Redux의 state에 접근할 수 있게 해주는 메서드이다.

import React from "react";
import { addToCart, notify } from "../actions/index";
// useDispatch와 useSelector 은 "react-redux"에서 import 한다. 
import { useSelector, useDispatch } from "react-redux"; 
import Item from "../components/Item";

function ItemListContainer() {
  const state = useSelector((state) => state.itemReducer); // useSelector로 현재 컴포넌트(ItemListContainer)와 Redux 저장소의 state를 연결해준다. 
  const { items, cartItems } = state;
  const dispatch = useDispatch(); // 컴포넌츠 안에서 useDispatch로 정의해준다.
  
/* 클릭했을 때 장바구니에 없으면 '장바구니에 추가되었습니다'가 뜨면서, 
Action 객체가 state를 바꾸고, 
있으면 '이미 추가된 상품입니다' 가 뜨는 이벤트 함수 */
    const handleClick = (item) => {
    if (!cartItems.map((el) => el.itemId).includes(item.id)) {
      
      // addToCart는 Action 객체를 생성하는 함수이다. 
      dispatch(addToCart(item.id)); // 호출 된 addToCart()의 리턴 값인 Action 객체가 dispatch()의 파라미터로 담긴다.
      
      dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
    } else {
      dispatch(notify("이미 추가된 상품입니다."));
    }
  };
  
  return <button onClick={()=>handleClick(item)}> 장바구니 추가 </button>
}

Reducer : state를 변화시킬 함수

Reducer는 Dispatch에게서 전달받은 Action 객체의 type 값에 따라서 상태를 변경시키는 함수이다.

import { initialState } from "./initialState";

// 위의 예제 코드에서 useSelector()로 연결한 state.itemReducer 이다.
const itemReducer = (state = initialState, action) => {
  switch (action.type) {
    case "ADD_TO_CART":
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload],
      });
    default:
      return state;
  }
};
      
// action객체의 type 프로퍼티 값이 "ADD_TO_CART"인 경우,
// cartItem 프로퍼티 값인 배열에
// action객체의 payload 프로퍼티값이 추가된다.
// action 객체의 payload 값은 위 Action 객체 설명의 예제 코드에 있는 { quantity: 1, itemId } 객체 이다.

Redux의 세 가지 원칙

① Single source of truth

동일한 데이터는 항상 같은 곳에서 가지고 와야 한다.
즉, Redux에는 데이터를 저장하는 Store라는 단 하나뿐인 공간이 있음과 연결이 되는 원칙이다.

② State is read-only

상태는 읽기 전용이라는 뜻으로, Redux의 상태도 직접 변경할 수 없다. 즉, Action 객체가 있어야만 상태를 변경할 수 있음과 연결되는 원칙이다.

③ Changes are made with pure functions

변경은 순수함수로만 가능하다는 뜻으로, 상태가 엉뚱한 값으로 변경되는 일이 없도록 순수함수로 작성되어야하는 Reducer와 연결되는 원칙이다.

Redux 공식 문서
React Redux 공식 문서

사용하다 직면한 문제

화면에서 수량 변경은 되는데, 이 부분이 테스트를 통과가 안됐다.
👉🏻 test 를 위한 문제일 뿐, 실제 작동하는데에는 문제없다는 답변을 받았다.
코드를 만지면서 action 객체 생성자 함수에 이미 작성되어 있던 코드를 내가 임의로 바꿔서 문제가 됐던 거였다.

테스트 불통과된 내용상 문제 원인 : itemIdquantity가 바뀌어 있다.
왜????

이유는 내가 순서를 거꾸로 거슬러 올라가면서 이미 작성되어 있던 코드를 바꿔서 테스트가 통과되지 않았던 거였다.
작성 되어 있던 코드를 건드리지 않고 그에 맞춰서 작성했으면 통과했을 것😂

원래 훑어봐야 하는 순서가
action 객체 (생성자 함수) index.js 파일
② 이벤트 핸들러 함수 정의된 곳 & 이벤트 핸들러 함수 바디의 dispatch() 함수 shopingCart.js 파일
③ 이벤트 핸들러 함수가 적용된 곳에서 전달받는 파라미터 CartItem.js 파일
순서대로 봐야 했는데,

나는 파일을 반대 순서대로 살펴보면서 하다가 ① action 객체(생성자 함수)에 전달되는 파라미터에 이미 setQuantity(quantity, itemId) 순서로 작성되어 있던 것을,
setQuantity(itemId, quantity)로 순서를 바꿔서 해당 테스트 통과가 안됐다.

// ③번째 파일 CartItem.js
<input
  type="number"
  min={1}
  className="cart-item-quantity"
  value={quantity}
  onChange={(e) => {
      handleQuantityChange(Number(e.target.value), item.id); 
        }} // 여기서 ❶ Number(e.target.value), ❷ item.id 순서로, 
// 즉, ❶ quantity, ❷ itemId 순서로 전달
      ></input>

수량 변경 이벤트가 일어나는 input의 onChange 에서
handleQuantityChange()함수로
첫 번째 파라미터로 입력된 숫자를,
두 번째 파라미터로 상품의 ID를 넘겨주는 코드는 이미 작성되어 있었다.

// ②번째 파일 shopingCart.js
  const handleQuantityChange = (quantity, itemId) // 여기도 ❶ 첫 번째 파라미터로 quantity, ❷ 두 번째 파라미터로 itemId를 작성해준다.
  => {
    dispatch(setQuantity(itemId, quantity)); // 여기서 주의! setQuantity( ) action객체 생성자 함수에 넘겨줄 때에는 
// setQuantity() 함수가 정의되어 있는 파일에 이미 파라미터 순서 코드가 작성되어 있었어서 그 순서에 맞춰서 넘겨줘야 한다.
  };

그래서 handleQuantityChange( ) 가 정의된 상위 컴포넌츠인 shopingCart.js 에 가서
첫 번째 파라미터로 quantity, 두 번째 파라미터로 itemId 를 코드로 작성해야한다.

dispatch()에도 Action 객체를 생성하는 함수(Action Creator)의 파라미터로 넘겨주는데,

이때 ! setQuantity() 함수가 정의되어 있는 곳에서도 이미 파라미터의 순서 코드가 작성되어 있었어서 그 코드에 맞춰서 파라미터의 순서를 바꿔서 넣어줘야한다.
즉, 첫 번째 파라미터로 itemId를,
두 번째 파라미터로 quantity를 넘겨주어야 한다.

// ①번째 파일 index.js
export const setQuantity = (itemId, quantity) => { // 여기서도 Id, quantity 순서로 작성한다.
  return {
    type: SET_QUANTITY,
    payload: { itemId, quantity },
  };
};

정리하자면,
Action 객체를 생성하는 함수의 파라미터의 순서와
dispatch()에 전달하는 함수의 파라미터의 위치가
동일하면 문제 없다.
(테스트 불통과 문제는 그냥 테스트를 위한 설정이었을 뿐..)


참고 : React Hooks에 취한다 - useReducer 확실히 정리해드려요 | 리액트 훅스 시리즈
코드스테이츠 유어클래스

profile
React, Next.js, TypeScript 로 개발 중인 프론트엔드 개발자

0개의 댓글