본문 바로가기

쇼핑몰 앱에서 Redux를 이용한 상태 관리 (1)

쇼핑몰 앱에서 Redux를 이용한 상태 관리 (1)
*  Redux의 설계(원리와 구조) 파악하기
-  Action, Dispatch, Reducer, Store 작성하고 연결하여 사용하기
-  Redux hooks(useSelector, useDispatch)를 사용해 Store 업데이트 하기

 

즉, React Hooks에서의 상태 변경 로직(이전 포스팅)이 Redux에서 어떻게 컴포넌트로부터 분리되는 지를 파악하는 것이 목적이다.

  • React 애플리케이션을 개발할 때 Redux를 사용하면 React 컴포넌트 간의 복잡한 데이터 흐름을 따라갈 필요가 없어진다. 특히 컴포넌트가 많아지고 애플리케이션의 구조가 고도화될수록 Redux를 활용한 상태 관리는 빛을 발한다.
  • 그동안에는 상태를 다루기 위해 컴포넌트 안에서 상태 변경 로직이 복잡하게 얽혀있는 경우가 많았다.
    그러나 상태 변경 로직을 컴포넌트로부터 분리하면 표현에 집중한, 보다 단순한 함수 컴포넌트로 만들 수 있다.

 


 

파일(컴포넌트) 구조 

 

이번 Cmarket Shopping App은 Create React App으로 만든 React 애플리케이션(이전 포스팅)에 Redux를 붙인 구조이다.

  • 아래는 Redux로 설계된 컴포넌트 간의 관계를 '장바구니에서 물건을 삭제하는 상황'을 바탕으로 도식화한 것이다.
  • 해당 글을 끝까지 읽고 다시 보면서 이해하는 것이 도움이 될 것이다. (포스팅의 마지막에 아래와 똑같은 그림을 넣어두었다.)

 

 

 


 

Redux 사용하기

 

1. Store 생성 

: 말 그대로 Redux 앱의 state가 저장 및 관리되는 오직 하나뿐인 저장소의 역할을 한다.

 

*  리팩토링 전 : createStore 메서드를 활용 (권장되지 않는 방법)

  • 다음은 createStore 메서드를 활용해 Reducer를 연결하는 방법이다.
  • createStore와 더불어 다른 Reducer의 조합을 인자로 넣어서 스토어를 생성할 수 있다.
리팩토링 전 코드 (나머지 코드는 리팩토링 후 코드와 함께 설명 예정)
// applyMiddleware은 redux에서 비동기를 편리하게 처리하기 위해 미들웨어를 생성하기 위해 필요한 api
// compose는 함수형 프로그래밍 유틸리티로, 여러 스토어 함수(enhancer)들을 순차적으로 적용하기 위해 사용
// (함수를 오른쪽에서 왼쪽으로 조합)
import { compose, createStore, applyMiddleware } from "redux";

// rootReducer는 Redux store의 초기 상태 및 액션에 대한 리듀서 함수
import rootReducer from '../reducers/index';
import thunk from "redux-thunk";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));

export default store;

 

  •  실제로 createStore을 import 해오면, 아래와 같이 취소선이 뜨는 것을 확인할 수 있다. 

 

  • createStore 함수에 취소선이 있는 이유는, Redux의 최신 버전에서는 configureStore 함수를 사용하는 것이 권장되는 방법이기 때문이다.
    • configureStore 함수는 Redux store를 구성하는 데 필요한 모든 기능을 자동으로 구성하며, 개발자가 일일이 설정할 필요가 없다.
    • 예를 들어, configureStore 함수는 Redux DevTools Extension을 자동으로 활성화하고, Redux Toolkit에서 제공하는 기능을 자동으로 설정한다.
  • 따라서 Redux의 최신 버전에서는 createStore 함수 대신 configureStore 함수를 사용하는 것이 매우 권장되며, createStore 함수를 사용하는 경우에는 더 이상 공식적으로 지원되지 않을 수 있다.
  • 하지만 createStore 함수는 여전히 사용 가능하며, 이전 버전의 Redux 코드와 호환성을 유지하기 위해 제공되고 있다.

 

나머지 코드에 대한 부연 설명  )

더보기
import { compose, createStore, applyMiddleware } from "redux";
// applyMiddleware은 redux에서 비동기를 편리하게 처리하기 위해 미들웨어를 생성하기 위해 필요한 api
// compose는 함수형 프로그래밍 유틸리티로, 여러 스토어 함수(enhancer)들을 순차적으로 적용하기 위해 사용
// (함수를 오른쪽에서 왼쪽으로 조합)
import thunk from "redux-thunk";

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?   // 🟣 (1)
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose;
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)));  // 🟣 (2)

 

🟣 (1) 코드는 Redux DevTools Extension을 사용할 수 있도록 Redux store의 enhancer를 구성하는 데 사용된다.

  • composeEnhancers는 Redux 개발 도구(DevTools)의 compose 함수를 확장하는 함수이다.
  • Redux는 Redux DevTools Extension을 사용하여 애플리케이션의 상태 변화를 시각적으로 디버깅할 수 있다.
    • 이 도구를 사용하기 위해서는 Redux DevTools의 확장 기능(compose 함수)을 Redux store에 연결해야 한다.
    • Redux DevTools를 사용하지 않는 경우에는 compose 함수만 사용하면 되지만, Redux DevTools를 사용하는 경우에는 compose 함수를 확장하는 composeEnhancers 함수를 사용하여 Redux store에 Redux DevTools를 연결해야 한다.
  • composeEnhancers 함수는 Redux DevTools Extension이 설치되어 있으면 그것을 사용하여 Redux store의 enhancer를 구성한다. 그렇지 않으면 Redux의 기본 compose 함수를 사용한다.

 

🟣 (2) 코드에서  Redux의 enhancer 함수는 createStore 함수를 호출할 때 두 번째 인수로 제공되다.

  • enhancer는 createStore 함수가 반환하는 새로운 버전의 store의 동작을 수정하는 함수이다. enhancer를 사용하면 Redux store에 미들웨어를 추가하거나 store의 기능을 확장할 수 있다.
  • 이러한 enhancer 함수는 Redux에서 기본적으로 제공되는 compose 함수를 사용하여 여러 enhancer를 하나로 결합할 수 있다. 
  • composeEnhancers 함수는 applyMiddleware 함수와 결합되어 Redux store의 enhancer를 결정한다. (applyMiddleware(thunk)는 Redux의 thunk 미들웨어를 적용하는 데 사용된다.)
  • applyMiddleware 함수는 Redux의 미들웨어를 적용하기 위해 사용되며, 미들웨어는 Redux store의 동작을 수정하거나 확장하는 데 사용된다. 미들웨어는 Redux store의 액션과 상태 사이에서 작동하며, 비동기 작업 및 로깅과 같은 작업을 처리하기에 적합하다.

 

🟣 (2) 코드에서  Redux의 thunk는 미들웨어의 하나로서 액션 객체 대신에 함수를 디스패치(dispatch)할 수 있게 한다.

  • Redux에서 thunk 이 함수는 dispatch와 getState를 인수로 받는다. 이 함수는 비동기 작업이 완료된 후에 액션 객체를 디스패치할 수 있다.
  • 예를 들어, Redux에서 서버에서 데이터를 가져오는 경우, 일반적으로 비동기 작업을 수행해야 한다. thunk를 사용하면 액션 생성자 함수를 일반 객체 대신 비동기 함수를 반환하여 디스패치할 수 있다. 이 함수는 데이터를 가져오는 작업을 수행하고, 데이터가 성공적으로 가져와지면 액션을 디스패치하여 애플리케이션의 상태를 업데이트한다.
  • 즉, Redux에서 thunk 미들웨어를 사용하면 애플리케이션에서 비동기 작업을 처리할 때 Redux store의 상태를 쉽게 관리할 수 있다. 

  

리팩토링 후 (configureStore 사용)
// Redux-Toolkit은 Redux를 만든 곳에서 공식적으로 효율적인 Redux 개발을 위해 만들어진 툴킷이다.
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from '../reducers/index';

// 리팩토링 전 코드와 비교했을 때, thunkMiddleware는 redux-thunk 라이브러리에서 기본 내보내기(default export)이다. 
// thunk는 동일한 라이브러리에서의 명명된 내보내기(named export)이며, thunkMiddleware에 대한 별칭(alias)이다. 
// 따라서 thunk를 가져와 thunkMiddleware처럼 미들웨어로 사용할 수도 있다.
import thunkMiddleware from 'redux-thunk';

const store = configureStore({
  reducer: rootReducer,
  middleware: [thunkMiddleware], // 🟣 (1)
  devTools: process.env.NODE_ENV !== 'production', // 🟣 (2) Redux DevTools 사용 여부 설정. (기본값은 true)
});

export default store;

 

  • 앞에서도 언급했지만, configureStore 함수는 Redux Toolkit에서 제공하는 기능을 자동으로 구성하며, Redux DevTools Extension을 자동으로 활성화한다. (devTools 옵션은 개발 환경에서만 Redux DevTools Extension을 활성화한다.)

🟣 (1) 코드에서 middleware 옵션을 사용하여 미들웨어를 설정할 수 있다. (composeEnhancers 함수를 사용하지 않아도 된다.)

  • configureStore 함수는 Redux의 기본 compose 함수를 사용하여 미들웨어를 결합한다.
  • 위 코드에서 middleware는 Redux에서 액션과 리듀서 사이에서 실행되는 미들웨어 함수들의 배열이다.
  • [thunkMiddleware]은 이 배열에 단일 미들웨어 함수로 thunkMiddleware를 추가하는 것을 의미한다. 
  • Redux 미들웨어는 액션과 리듀서 사이에서 실행되며, 액션이 디스패치(dispatch)되기 전에 액션을 수정하거나, 액션을 가로채서 다른 동작을 수행하거나, 액션을 무시하거나, 액션의 결과를 로깅하거나, 비동기 작업을 처리하는 등의 기능을 수행할 수 있다.

 

🟣 (2) 코드에서 devTools: process.env.NODE_ENV !== 'production'는 현재 애플리케이션이 개발 환경이 아닌 경우에만 Redux 개발 도구를 활성화하는 것을 의미한다.

  • process.env.NODE_ENV는 현재 Node.js 프로세스의 환경 변수(environment variable) 중 하나로, 현재 애플리케이션이 실행 중인 환경을 나타낸다.
  • 일반적으로 개발 환경에서는 'development', 프로덕션 환경에서는 'production'으로 설정된다.
  • 위의 코드에서 devTools는 Redux 개발 도구를 사용할지 여부를 결정하는데, 이를 위해 process.env.NODE_ENV를 검사하고 'production'이 아닌 경우에만 Redux 개발 도구를 활성화한다. 이렇게 하면 프로덕션 환경에서는 Redux 개발 도구를 비활성화하여 애플리케이션의 성능을 개선할 수 있다.

 

2. Action 생성자 함수 생성

: Action은 말 그대로 어떤 액션을 취할 것인지 정의해 놓은 객체이다. (아래 예시 참고)

  • type은 필수로 지정을 해 주어야 하며, 그 외의 것들은 선택적으로 사용할 수 있다.
  • 이렇게 모든 변화를 Action을 통해 취하는 것은 우리가 만드는 앱에서 무슨 일이 일어나고 있는지 직관적으로 알기 쉽게 하는 역할을 한다.
{ type: ‘ADD_TO_CART’, payload: request }

 

(1) type 값을 변수로 만들기

  • 오타 등의 실수를 줄이기 위함이다.
// action types
export const ADD_TO_CART = "ADD_TO_CART";
export const REMOVE_FROM_CART = "REMOVE_FROM_CART";
export const SET_QUANTITY = "SET_QUANTITY";
export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";

 

자동 완성이 뜨는 모습

 

(2) 액션 생성자 함수 생성

// actions creator functions
// 장바구니에 물건을 추가할 때
export const addToCart = (itemId) => {
  return {
    type: ADD_TO_CART,
    payload: { quantity: 1, itemId }
  }
}

// 장바구니에서 물건을 삭제할 때
export const removeFromCart = (itemId) => {
  return {
    type: REMOVE_FROM_CART,
    payload: { itemId }
  }
}

// 장바구니에서 수량을 변경할 때
export const setQuantity = (itemId, quantity) => {
  return {
    type: SET_QUANTITY,
    payload: { itemId, quantity }
  }
}

 

3. Reducer 작성

  • Reducer는 현재의 state와 Action을 이용해서 새로운 state를 만들어 내는 순수함수이다. 
  • if문으로 작성해도 무방하지만, 성능을 고려하면 switch문을 통해서 코드를 작성하는 것이 좋다.

 

[Reducer의 Immutability(불변성)]

Reducer 함수를 작성할 때 주의해야 할 점이 있다.
바로 Redux의 state 업데이트는 immutable한 방식으로 변경해야 한다는 것이다.

-  Redux의 장점 중 하나인 변경된 state를 로그로 남기기 위해서 꼭 필요한 작업이다.
-  immutable한 방식으로 state를 변경하기 위해서는 Object.assign을 통해 새로운 객체를 만들어 리턴하는 것 등이 있다.

 

itemReducer.js
import { REMOVE_FROM_CART, ADD_TO_CART, SET_QUANTITY } from "../actions/index";
import { initialState } from "./initialState";

const itemReducer = (state = initialState, action) => {

  switch (action.type) {
    case ADD_TO_CART:
      return Object.assign({}, state, {
        cartItems: [...state.cartItems, action.payload] // 추가만 함.
      })
      // {...state, cartItems : [...state.cartItems, action.payload ]} 이렇게도 된다.
    
      case REMOVE_FROM_CART:
      let currentItem = state.cartItems.filter((el) => el.itemId !== action.payload.itemId)
      return Object.assign({}, state, {
        cartItems: currentItem
      })
      // {...state, cartItems: state.cartItems.filter(e => e.itemId !== action.payload.itemId)}
    
      case SET_QUANTITY:
      let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
      return {
        ...state,
        cartItems: [...state.cartItems.slice(0, idx), action.payload, ...state.cartItems.slice(idx + 1)]
      }

      // 2가지 방법이 더 있다.
      
      // {state, cartItems: state.cartItems.map((e, i) => i === idx ? action.payload) : e}

      // let cartItems = state.cartItems;
      // cartItems[idx].quantity = action.payload.quantity;
      // return {...state, cartItems}

    default:
      return state;
  }
}

export default itemReducer;

 

다음 포스팅에 나올 다른 Reducer(notificationReducer)도 있기 때문에 rootReducer로 합쳐준다.

실제로 store을 만들 때, 이 rootReducer가 첫 번째 인자로 들어갔음을 확인할 수 있다.

import { combineReducers } from 'redux';
import itemReducer from './itemReducer';
import notificationReducer from './notificationReducer';

const rootReducer = combineReducers({  // 2개의 Reducer을 합쳤다.
  itemReducer,
  notificationReducer
});

export default rootReducer;

 

 

 

4. Dispatch 적용

  • Dispatch는 Action을 전달하는 메서드이다.
  • Dispatch의 전달인자로 Action 객체가 전달된다.
  • 그리고 Reducer를 호출해 state의 값을 바꾸는 역할을 한다.
  • Redux hooks 중 하나인 useDispatch를 활용해 필요한 컴포넌트에 넣어준다. 
  • (useDispatch 파트에 Dispatch 코드가 있다.)

 

필요한 요소는 다 준비 되었다. 이제 Redux Hooks로 각각의 개념들을 연결시켜 준다.

 

 

4-1. useSelector()

  • 먼저 useSelector()는 컴포넌트와 state를 연결하는 역할을 한다.
  • 컴포넌트에서 useSelector 메서드를 통해 Store의 state에 접근할 수 있는 것이다.
  • useSelector의 전달인자로는 콜백 함수를 받으며 콜백 함수의 전달인자로는 state 값이 들어간다.
  • 자세한 사용법은 공식 문서의 useSelector examples를 참고하는 게 좋다.

 

ItemListContainers.js (일부)
import React from 'react';

// useSelector 불러온다.
import { useSelector } from 'react-redux';
import Item from '../components/Item';

function ItemListContainer() {
  // state 객체 안에서 itemReducer의 값을 꺼내온다.
  const state = useSelector(state => state.itemReducer);
  
  // 그 안에서 items, cartItems를 꺼내온다.
  const { items, cartItems } = state;

 

const state = useSelector (state => state) 일 때, console.log(state

 

const state = useSelector (state => state.itemReducer) 일 때, console.log(state

 

 

4-2. useDispatch()

  • useDispatch()는 Action 객체를 Reducer로 전달해 주는 메서드이다.
  • Action이 일어날만한 곳은 클릭 등의 이벤트가 일어나는 컴포넌트이다.
  • 공식 문서의 useDispatch() examples를 통해 Dispatch 메서드에 전달인자로 Action이 어떻게 전달되는지 확인해본다.

 

ItemListContainers.js (완성 코드)
import React from 'react';

// 액션 생성자 함수 중 addToCart를 불러온다.
import { addToCart } from '../actions/index';

// useSelector, useDispatch를 불러온다.
import { useSelector, useDispatch } from 'react-redux';

// 자식 컴포넌트 Item 
import Item from '../components/Item';

function ItemListContainer() {
  const state = useSelector(state => state.itemReducer);
  const { items, cartItems } = state;
  const dispatch = useDispatch();

  const handleClick = (item) => {
    if (!cartItems.map((el) => el.itemId).includes(item.id)) {
      dispatch(addToCart(item.id))      // dispatch 함수에 액션객체를 인자로 전달한다.
  }

return (
    <div id="item-list-container">
      <div id="item-list-body">
        <div id="item-list-title">쓸모없는 선물 모음</div>
        {items.map((item, idx) => <Item item={item} key={idx} 
        handleClick={handleClick} />)}
      </div>
    </div>
  );
}

export default ItemListContainer;

 

 

파일(컴포넌트) 구조 다시 보기

 

아래는 Redux로 설계된 컴포넌트 간의 관계를 '장바구니에서 물건을 삭제하는 상황'을 바탕으로 도식화한 것이다.

 


 

이로서 Redux의 기본 개념을 바탕으로 장바구니 추가 등 쇼핑몰 기능을 구현했다.

두 번째 포스팅에서는 해당 앱에서 구현한 또 다른 Reducer인 notificationReducer에 대해 다뤄보겠다.

728x90
⬆︎