본문 바로가기

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

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

본격적으로 본론에 들어가기 전에,

npm trends를 살펴보면 상태 관리 라이브러리 중에서도 

redux(하늘색)가 가장 많이 쓰이고 있는 것을 알 수 있다.

 

 

따라서, 비교적 간편한 Redux toolkit을 쓰기 전에, 기존 Redux 코드를 작성해보며

데이터 흐름을 숙지하고, 개념을 확실히 이해하고 넘어가야 할 필요가 있다.

 

 

 

 

장바구니 추가, 삭제 등 기본적인 쇼핑몰 기능을 구현한  이전 포스팅  에서 더 나아가,
두 번째 포스팅에서는 해당 앱에서 구현한 또 다른 Reducer인 notificationReducer 등에 대해 다뤄보겠다.

 

 

 


 

결과 화면

 

  • 장바구니에 없는 물건을 '장바구니 담기'를 눌렀을 때 -> "장바구니에 ~~이(가) 추가되었습니다." 메시지가 뜬다.
  • 장바구니에 이미 있는 물건을 또 '장바구니 담기'를 눌렀을 때 -> "이미 추가된 상품입니다." 메시지가 뜬다.

 

 


 

Redux 사용하기

 

1. Store 생성 

:전역 상태 저장소 Store는 만들어진 상태니, 액션 생성자 함수를 만드는 것부터 시작한다.

 

2. Action 생성자 함수 생성

 

export const NOTIFY = "NOTIFY";
export const ENQUEUE_NOTIFICATION = "ENQUEUE_NOTIFICATION";
export const DEQUEUE_NOTIFICATION = "DEQUEUE_NOTIFICATION";

export const notify = (message, dismissTime = 5000) => dispatch => {  // 🟣 (1)
  const uuid = Math.random()
  dispatch(enqueueNotification(message, dismissTime, uuid))
  setTimeout(() => {
    dispatch(dequeueNotification())
  }, dismissTime)
}

// 메시지 생성
export const enqueueNotification = (message, dismissTime, uuid) => { // 🟣 (2)
  return {
    type: ENQUEUE_NOTIFICATION,
    payload: { message, dismissTime, uuid }
  }
}

// 메시지 삭제
export const dequeueNotification = () => {
  return {
    type: DEQUEUE_NOTIFICATION
  }
}

 

🟣 (1)의 코드는 Redux의 thunk 액션 생성자 함수로, 비동기 작업을 처리하기 위해 사용된다. 

---> notify 함수는 2개의 매개변수를 받아서 함수를 반환하고, 이 반환된 함수는 dispatch 메서드를 매개변수로 받아 사용한다.

Redux thunk에서 dispatch 된 액션이 함수인지 아닌지 판단한다. 

Redux Thunk -> 함수인가? -> 아니요.(Plain Object) -> dispatch -> Reducer
Redux Thunk -> 함수인가? -> 예 -> 함수실행 -> dispatch -> 함수인가? -> 아니요(Plain Object) -> Reducer

[일반적인 형태]
export const fetchData = () => async dispatch =>{
   const response = await APIadress.get('/data');
   dispatch({ type: 'FETCH_DATA', payload: response }) }
  • notify 함수는 message와 dismissTime이라는 두 개의 매개변수를 받는다.
    • message는 알림 메시지를 나타내며, dismissTime은 알림이 표시되는 시간(밀리초)을 나타낸다.
    • dismissTime이 지난 후 알림이 자동으로 사라진다. (dismissTime이 생략된 경우, 기본값으로 5000ms(5초)가 지정)
  • notify 함수는 thunk 함수이므로, Redux의 dispatch 함수를 매개변수로 받는다.
    • 이 함수를 통해 액션을 디스패치(dispatch)하고, 액션 객체를 반환하는 함수를 반환한다.
  • notify 함수 내부에서는 먼저, Math.random()을 사용하여 랜덤한 UUID(Universally Unique Identifier)를 생성한다.
    • 그리고 enqueueNotification 액션 생성자 함수를 호출하여 알림을 큐(queue)에 추가하고, 생성된 UUID를 액션 객체에 추가한다. 이렇게 하면 알림이 표시되는 순서를 유지할 수 있다.
    • 그 다음, setTimeout 함수를 사용하여 dismissTime이 지난 후에 dequeueNotification 액션을 디스패치하여 큐에서 알림을 제거한다.
  • 즉, notify 함수는 알림 메시지를 큐에 추가하고, 일정 시간이 지난 후에 알림을 자동으로 사라지게 하는 기능을 수행한다.

 

3. Reducer 작성

 

notificationReducer.js
import { ENQUEUE_NOTIFICATION, DEQUEUE_NOTIFICATION } from "../actions/index";

const notificationReducer = (state = {notifications:[]}, action) => {

  switch (action.type) {
  
    case ENQUEUE_NOTIFICATION:  // Object.assign을 사용한 첫 번째 방법
      return Object.assign({}, state, {
        notifications: [...state.notifications, action.payload]  
      });
      
      // spread syntax를 사용한 두 번째 방법  (+ ...state가 딱히 필요한 상황이 아니므로 리팩토링)
      // { notifications: [...state.notifications, action.payload] };
      
    case DEQUEUE_NOTIFICATION:  
      return {
        ...state,  // 이전 객체를 참조하지 않아도 되는 경우이므로 이 줄은 생략 가능
        notifications: state.notifications.slice(1)  // 알림 목록에서 가장 오래된 알림을 제거
      };
      
    default:
      return state;
  }
}

export default notificationReducer;
  • notificationReducer 함수는 초기 상태 값으로 notifications 배열을 포함하는 객체를 가진다.
  • notifications 배열은 {message, dismissTime, uuid}가 담긴, 알림 메시지를 나타내는 객체들을 담는다.
    1. 첫 번째 case에서는 ENQUEUE_NOTIFICATION 액션이 발생했을 때의 동작을 정의한다. action.payload는 액션 객체에 추가로 담긴 데이터를 나타내며, 여기서는 새로운 알림 메시지 객체 {message, dismissTime, uuid} 를 담고 있다. notifications 배열에 새로운 알림 메시지 객체를 추가한 후 새로운 상태 객체를 반환한다.
    2. 두 번째 case에서는 DEQUEUE_NOTIFICATION 액션이 발생했을 때의 동작을 정의한다. state.notifications.slice(1)은 notifications 배열의 첫 번째 요소를 제외한 나머지 요소들을 담은 새로운 배열을 반환한다. 
    3. 마지막 default 문은 action.type에 일치하는 케이스가 없을 때 기존 상태를 반환하는 역할을 한다.

 

4. Dispatch 적용

import React from 'react';
import { addToCart, notify } from '../actions/index';  // notify를 import
import { useSelector, useDispatch } from 'react-redux';
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(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`))  // 장바구니에 해당 상품이 없을 때,
    }
    else {
      dispatch(notify('이미 추가된 상품입니다.'))  // 장바구니에 해당 상품이 있을 때
    }
  }

  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;

 

5. 메시지 창 components

 

NotificationCenter.js (부모)
import { useSelector } from 'react-redux';
import Toast from './Toast';

function NofiticationCenter() {
  const state = useSelector(state => state.notificationReducer);
return <div className="notification-container top-right">
    {
      state.notifications.map((n) =>
        <Toast key={n.uuid} text={n.message} dismissTime={n.dismissTime} /> <!-- 알림창 컴포넌트 -->
      )
    }
  </div>
}

export default NofiticationCenter
  • 이 코드는 Redux store에서 상태를 가져와 state 변수에 할당하고, notifications 배열을 매핑하여 Toast 컴포넌트를 렌더링하는 NotificationCenter 컴포넌트를 정의한다.
  • 알림을 관리하는 데 필요한 모든 구성 요소를 포함하고 있으며, 알림을 표시하는 데 필요한 모든 데이터와 로직을 관리한다. 
    1. useSelector 훅을 사용하여 Redux store에서 notificationReducer의 상태를 가져온다.
    2. notifications 배열을 매핑하여 Toast 컴포넌트를 렌더링한다. key 속성으로 고유한 uuid를 사용한다.
    3. NotificationCenter 컴포넌트는 notification-container 클래스를 사용하여 CSS 스타일을 적용하고, top-right 클래스를 사용하여 오른쪽 상단에 알림을 표시한다.

 

Toast.js (자식)
import React, { useEffect, useState } from 'react'

export default function Toast({ text, dismissTime }) {
  const [isFading, setIsFading] = useState(false)

  useEffect(() => {
    let mounted = true
    setTimeout(() => {
      if (mounted) { setIsFading(true) } }, dismissTime - 500) // dismissTime이 5초라면 4.5초 뒤에 실행

    return () => { mounted = false }
  }, [])

  return (
    <div className={`notification ${isFading ? 'fade-out' : ''}`}>
      {text}
    </div>
  )
}
  • Toast 컴포넌트는 전달된 text와 dismissTime props를 사용하여 알림을 표시하고, isFading state를 사용하여 알림을 페이드 아웃 처리한다.
    1. Toast 컴포넌트는 text와 dismissTime 두 개의 프롭스(props)를 받는다.
      (text 프롭스는 알림 메시지를 나타내는 문자열이며, dismissTime 프롭스는 알림이 표시되는 시간(밀리초)을 나타내는 숫자)
    2. 컴포넌트 내부에서는 React의 useState 훅을 사용하여 isFading 상태를 관리한다.
      isFading은 알림이 페이드아웃(fade-out) 중인지를 나타내는 불리언(boolean) 값이다.
    3. 그리고 useEffect 훅을 사용하여 알림이 표시된 후 일정 시간이 지나면 isFading 값을 true로 변경하여 알림이 페이드아웃되도록 한다. 이 때, setTimeout 함수를 사용하여 일정 시간이 지난 후 setIsFading(true) 함수가 호출되도록 한다.
    4. 마지막으로, return 구문에서는 알림 컴포넌트가 마운트되어 있는 동안에만 setTimeout 함수가 실행되도록 하고, 컴포넌트가 언마운트될 때 mounted 값을 false로 설정하여 setTimeout 함수가 실행되지 않도록 한다.

 

관련 CSS
.notification {
  transition: transform 0.6s ease-in-out;
  animation: toast-in-right 0.6s;
  background: var(--coz-purple-600);
  transition: 0.3s ease;
  border-radius: 20px;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
  color: #000;
  opacity: 0.9;
  font-weight: 600;

  height: 50px;
  width: 360px;
  color: #fff;
  padding: 15px;
  margin: 10px;
}

.fade-out {
  opacity: 0;
  transform: opacity 2s;
}
  • notification 클래스는 알림의 기본 스타일을 정의한다.
  • fade-out 클래스는 알림이 페이드아웃되는 동안 적용될 스타일을 정의한다.

 


기타 깨달음의 메모

 

redux devTools

 

콘솔에 찍어 보는 것처럼, 상태 관리도 브라우저에서 확인할 수 있어서 유용한 크롬 익스텐션이다.

아래 그림처럼 별개의 창으로 띄워 쓸 수도 있고

 

 

개발자 코드에서 바로 사용도 가능하다.

 

 

 

 

React 앱 렌더링 방식

 

첫 번째 코드 (React 18 이상의 버전의 최신 방식)
const rootElement = document.getElementById('root');
const root = createRoot(rootElement);

root.render(
  <Provider store={store}>
  <App />
  </Provider>
);
  • createRoot API를 사용하여 React 18의 Concurrent Mode를 활용한 코드이다. 이를 사용하면 React가 더욱 효율적으로 렌더링을 처리할 수 있다.
  • createRoot를 사용하여 렌더링된 요소는 애플리케이션의 전체 트리에 대한 렌더링 중인 요소로 처리된다. 이것은 병렬로 처리할 수 있는 모든 작업을 최대한 병렬로 처리하여 애플리케이션의 성능을 향상시키는 데 도움이 된다.

 

두 번째 코드 (React 17 이하의 버전)
import React from 'react';
import ReactDOM from 'react-dom';
.
.
.
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  • React 17 이하의 버전에서 사용되는 코드이다. 이 코드는 ReactDOM.render를 사용하여 애플리케이션을 렌더링한다.
  • 결론은, React 18 이상의 버전에서는 첫 번째 코드를 사용하는 것이 더욱 효율적이다. 그러나 React 17 이하의 버전에서는 두 번째 코드를 사용해야 한다.

 

serviceWorker?

 

최상위 index.js에 이런 문구가 적혀 있어, 뜻을 찾아보았다.

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();

 

서비스 워커는 브라우저에서 백그라운드에서 실행되는 스크립트로, 웹 애플리케이션의 네트워크 요청을 가로채고 캐싱하여 오프라인 상태에서도 작동할 수 있도록 한다.

  • serviceWorker.unregister()는 React 애플리케이션에서 서비스 워커를 등록하지 않도록 해주는 코드이다.
    • create-react-app에서는 기본적으로 서비스 워커가 등록되어 있어, 애플리케이션이 처음 로드될 때 서비스 워커가 설치된다. 
    • 그러나, serviceWorker.unregister()를 사용하면 서비스 워커가 등록되지 않도록 하여 사용자가 불필요한 캐시를 사용하지 않도록 할 수 있다.
  • 만약 서비스 워커를 등록하고자 한다면, serviceWorker.register()를 사용하면 된다.
    • 이렇게 함으로써 웹 애플리케이션을 오프라인 상태에서도 사용할 수 있으며, 사용자 경험을 향상시킬 수 있다.
    • 서비스 워커는 네트워크 요청을 가로채서 캐싱하여, 애플리케이션의 성능을 향상시킨다. 
    • 하지만 다음과 같은 단점이 있다.
      1. 서비스 워커는 네트워크 요청을 가로채서 캐싱하여, 애플리케이션의 성능을 향상시킨다. 그러나, 서비스 워커가 캐시를 사용하게 되면, 사용자가 최신의 데이터를 보지 못하는 문제가 발생할 수 있다. 
      2. 서비스 워커는 애플리케이션에 추가적인 코드를 추가하므로, 애플리케이션의 용량을 증가시킨다. 
      3. 디버깅 용이성 서비스 워커는 네트워크 요청을 가로채므로, 디버깅이 어렵다. 
  • 결론적으로, 서비스 워커를 사용하지 않아도 되는 경우에는 서비스 워커를 등록하지 않는 것이 좋다.

 

ESLint 룰 검사?

 

useEffect 함수 안에 주석
useEffect(() => {
    let mounted = true
    setTimeout(() => {
      if (mounted) { setIsFading(true) } }, dismissTime - 500)

    return () => { mounted = false }
    // eslint-disable-next-line react-hooks/exhaustive-deps  // 🟣 해당 주석에 관한 설명
  }, [])
  • eslint-disable-next-line 주석은 ESLint 룰 검사를 잠시 동안 끄기 위해 사용되는 주석이다.
  • React의 useEffect 훅에서는 종종 해당 훅이 의도한 대로 작동하지 않을 때 발생하는 경고 메시지가 있다.
  • 이 경우 eslint-disable-next-line react-hooks/exhaustive-deps 주석을 사용하여 해당 경고를 무시할 수 있다.
    • react-hooks/exhaustive-deps 는 React 훅에서 의존성 배열이 충분하지 않을 때 경고를 발생시키는 ESLint 룰 중 하나이다.
    • 이를 해결하기 위해서는 useEffect 훅에서 사용하는 변수를 의존성 배열에 추가하거나, 훅의 두 번째 매개변수로 빈 배열을 전달하여 훅이 마운트된 후 한 번만 실행되도록 설정해야 한다.

 

Redux로 여러 상태 관리?

 

Redux로 여러개의 상태를 관리해야 하는 경우엔 다음과 같은 방법이 있다.


1. 하나의 reducer에 객체 형태로 상태를 관리하기

  - 초기 상태값 객체 안에 여러 키를 넣어 관리하는 방법이다.

initialState.js
export const initialState =
{
  "items": [
    {
      "id": 1,
      "name": "노른자 분리기",
      "img": "../images/egg.png",
      "price": 9900
    },
.
.
.
  ],
  "cartItems": [
    {
      "itemId": 1,
      "quantity": 1
    },
    .
    .
    .
  ]
}

 

itemReducer.js
import { initialState } from "./initialState";

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


2. 여러 개의 reducer로 상태 관리하기

  - 필요한 상태만큼 reducer를 만들어 관리하는 방법이다.
  - 이렇게 여러개의 reducer가 있는 경우 combineReducer 메서드로 합친 다음  store를 생성할 때 전달해준다.

 

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

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

export default rootReducer;

 

store.js
import { configureStore } from '@reduxjs/toolkit';
import rootReducer from '../reducers/index';
.
.

const store = configureStore({
  reducer: rootReducer,
  .
  .


여러 개의 reducer를 만들었을 때, dispatch가 발생하면 모든 reducer가 action을 전달받는다.

 

728x90

'FE > React' 카테고리의 다른 글

[React] Virtual DOM  (0) 2023.05.19
[JS] useEffect 용례  (0) 2023.05.04
쇼핑몰 앱에서 Redux를 이용한 상태 관리 (1)  (0) 2023.04.26
Redux로 간단한 Count 기능 구현하기  (0) 2023.04.24
상태 관리 라이브러리 Redux  (0) 2023.04.24
⬆︎