본문 바로가기
프론트엔드/리액트

[React] 키오스크 미니 프로젝트 피드백

by AI읽어주는남자 2025. 9. 24.
반응형

리액트의 라우터(컴포넌트 관리)와 리덕스(전역함수), 퍼시스턴스(스토리지)를 활용하는 실습 중에 AI에게 도움받은 마크다운 파일들입니다.

 

 

첫 번째 문제점 : 메뉴페이지에서 함수가 정상 작동하지 않음

미니 키오스크 개선 가이드

안녕하세요! 미니 키오스크 프로젝트의 장바구니 기능 구현에 어려움을 겪고 계신 부분을 도와드리겠습니다. MenuPage.jsxaddCart 함수와 cartSlice.jsx의 리듀서 로직을 중심으로 개선 방안을 제안합니다.

1. MenuPage.jsx 개선

현재 MenuPage.jsxuseRef와 여러 개의 <form>을 사용하여 클릭된 메뉴를 식별하려고 하고 있어 비효율적이고 정상적으로 작동하지 않습니다. 각 버튼이 클릭될 때 해당 메뉴 정보를 직접 addCart 함수로 넘겨주는 방식으로 간단하게 해결할 수 있습니다.

문제점:

  • map 안에서 동일한 ref를 여러 <form>에 할당하고 있습니다.
  • addCart 함수가 어떤 버튼이 클릭되었는지 식별할 방법이 없습니다.
  • formRef.current.elements['coffee'].value는 원하는 값을 가져올 수 없습니다.

개선된 코드 (MenuPage.jsx):

import { useDispatch } from "react-redux";
import { add } from "../store/cartSlice"; // add 액션 함수를 직접 import 합니다.

export default function MenuPage(props) {
    const dispatch = useDispatch();

    const menu = [
        { id: 1, name: "아메리카노", price: 3000 },
        { id: 2, name: "카페라떼", price: 4000 },
        { id: 3, name: "카푸치노", price: 4500 },
    ];

    // [수정] 장바구니 담기 함수: 클릭된 메뉴 객체를 인자로 받습니다.
    const addCart = (item) => {
        // dispatch를 통해 add 액션을 실행하고, payload로 메뉴 객체를 전달합니다.
        dispatch(add(item)); 
        console.log("장바구니에 담긴 상품:", item);
    };

    return (
        <>
            <h3>메뉴 페이지</h3>
            <div>
                {menu.map((m) => (
                    // [수정] 불필요한 form 태그를 제거하고, 각 버튼의 onClick에서 addCart 함수를 호출합니다.
                    // 이 때, 화살표 함수를 사용해 클릭된 메뉴(m)를 인자로 넘겨줍니다.
                    <div key={m.id} style={{ display: "flex", margin: "10px" }}>
                        <button type="button" onClick={() => addCart(m)}>
                            {m.name} ({m.price}원) 담기
                        </button>
                    </div>
                ))}
            </div>
        </>
    );
}

2. cartSlice.jsx 로직 수정

현재 add 리듀서는 장바구니에 상품을 추가하는 것이 아니라, 전체 cartInfo 배열을 새로 전달된 상품으로 교체하고 있습니다. 또한, 동일한 상품을 여러 번 담았을 때 수량을 증가시키는 로직이 필요합니다.

문제점:

  • state.cartInfo = action.payload는 기존 장바구니 내용을 덮어씁니다.
  • 수량(quantity) 개념이 없어 동일 상품을 구분할 수 없습니다.

개선된 코드 (cartSlice.jsx):

import { createSlice } from "@reduxjs/toolkit";

const initialState = {
    cartInfo: [],
};

const cartSlice = createSlice({
    name: 'cart',
    initialState,
    reducers: {
        // [수정] add 리듀서 로직 변경
        add: (state, action) => {
            // action.payload로 클릭된 메뉴 객체(예: {id: 1, name: "아메리카노", ...})가 들어옵니다.
            const newItem = action.payload;

            // 1. 장바구니에 이미 들어있는 상품인지 확인합니다.
            const existingItem = state.cartInfo.find((item) => item.id === newItem.id);

            if (existingItem) {
                // 2. 이미 있는 상품이면, 수량(quantity)만 1 증가시킵니다.
                existingItem.quantity++;
            } else {
                // 3. 없는 상품이면, quantity: 1 속성을 추가하여 cartInfo 배열에 새로 추가합니다.
                state.cartInfo.push({ ...newItem, quantity: 1 });
            }
        },
        // (옵션) 장바구니에서 아이템을 제거하거나 수량을 조절하는 리듀서를 추가할 수 있습니다.
        // remove: (state, action) => { ... }
    }
});

export default cartSlice.reducer;
export const { add } = cartSlice.actions; // 수정된 add 액션을 export 합니다.

참고: cartSlice.jsx에서 addexport const { add } = cartSlice.actions; 와 같이 named export 하고 있으므로, MenuPage.jsx에서는 import { add } from ... 로 가져와야 합니다.


3. CartPage.jsx 구현

수정된 cartSlice를 바탕으로 CartPage.jsx에서 장바구니 목록과 총합계를 아래와 같이 구현할 수 있습니다.

구현 예시 (CartPage.jsx):

import { useSelector } from "react-redux";

export default function CartPage(props) {
    // store의 cart 슬라이스에서 cartInfo 상태를 가져옵니다.
    const cartItems = useSelector((state) => state.cart.cartInfo);

    // 총 합계 계산
    const totalPrice = cartItems.reduce((sum, item) => {
        return sum + (item.price * item.quantity);
    }, 0);

    return (
        <>
            <h3>장바구니 페이지</h3>
            <div>
                {cartItems.length === 0 ? (
                    <p>장바구니가 비어있습니다.</p>
                ) : (
                    cartItems.map((item) => (
                        <div key={item.id}>
                            <span>{item.name}</span>
                            <span> | 수량: {item.quantity}개</span>
                            <span> | 가격: {item.price * item.quantity}원</span>
                        </div>
                    ))
                )}
            </div>
            <hr />
            <div>
                <h4>총 합계: {totalPrice.toLocaleString()}원</h4>
            </div>
        </>
    );
}

4. main.jsx 설정 확인

redux-persist가 올바르게 작동하려면 main.jsx (또는 index.js) 파일에서 <Provider><PersistGate>App 컴포넌트를 감싸고 있어야 합니다. 아래와 같은 구조인지 확인해보세요.

설정 예시 (main.jsx):

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './example/day06/App.jsx'
import './index.css'

// store와 persistor import
import store, { persistor } from './example/day06/실습8/store/store.jsx'
import { Provider } from 'react-redux'
import { PersistGate } from 'redux-persist/integration/react'

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>,
)

이 가이드라인을 따라 코드를 수정하시면 원하시는 장바구니 기능이 정상적으로 동작할 것입니다.

 

 

두 번째 문제점 : 장바구니페이지에서 총 합계 출력에 이상한 값이 나오고, [object Object] 출력이 반복됨

미니 키오스크 디버깅 가이드 (후속 조치)

안녕하세요. 총합계 계산 오류와 [object] 출력 문제에 대해 알려주셔서 감사합니다. 두 문제 모두 이전에 저장된 데이터 형식과 관련이 있을 가능성이 매우 높습니다.

문제 원인 분석

  1. 이상한 숫자 (NaN) 문제: redux-persist는 앱의 상태를 브라우저의 localStorage에 저장합니다. 이전에 작성하셨던 코드(수정 전 코드)가 cartInfo에 객체 배열이 아닌 다른 값(예: 단순 문자열 ID)을 저장했을 것입니다. 코드를 수정한 후에도 앱이 localStorage에 남아있는 예전 형식의 데이터를 불러오기 때문에, item.priceitem.quantity 같은 속성을 찾지 못해(undefined) 계산 과정에서 NaN (Not a Number)이 발생합니다. 이것이 "이상한 숫자"의 정체일 가능성이 높습니다.
  2. [object] 출력 문제: 이 문제는 자바스크립트 객체를 문자열처럼 화면에 표시하려고 할 때 발생합니다. 예를 들어 cartInfo에 상품 객체가 아닌 다른 객체가 들어갔거나, 렌더링하는 과정에서 item.name처럼 정확한 속성을 지정하지 않고 item 객체 자체를 출력하려고 할 때 나타날 수 있습니다.

해결 방안

가장 확실하고 간단한 해결책은 redux-persist가 저장한 localStorage의 데이터를 삭제하여 장바구니 상태를 초기화하는 것입니다.

단계별 실행 방법 (Chrome 브라우저 기준):

  1. 키오스크 앱이 실행된 페이지에서 F12 키를 눌러 개발자 도구를 엽니다.
  2. 상단 탭에서 Application을 선택합니다. (만약 보이지 않는다면 >> 아이콘을 눌러 찾아보세요.)
  3. 왼쪽 메뉴에서 Storage > Local Storage를 클릭한 후, 현재 앱의 주소 (예: http://localhost:5173)를 선택합니다.
  4. 오른쪽에 키(Key)와 값(Value) 목록이 나타납니다. keypersist:cart인 항목을 찾으세요.
  5. 해당 항목을 마우스 오른쪽 버튼으로 클릭하고 Delete를 선택하여 삭제합니다. (또는 해당 항목을 선택하고 Delete 키를 누릅니다.)
    • 만약 항목을 찾기 어렵다면, 그냥 모든 키를 삭제해도 무방합니다. 원 모양의 'Clear all' 아이콘 (🚫)을 클릭하면 됩니다.
  6. 페이지를 새로고침(F5)합니다.

이제 장바구니가 완전히 비워진 초기 상태에서 시작되며, 새로 추가하는 상품부터는 개선된 코드에 따라 올바른 형식으로 데이터가 저장될 것입니다. 이렇게 하면 총합계 계산이 정상적으로 작동해야 합니다.

[object] 문제 추가 확인

위 방법으로도 [object] 문제가 해결되지 않는다면, CartPage.jsxmap 부분이 아래 예시와 정확히 일치하는지 다시 한번 확인해주세요. item.name, item.quantity 처럼 객체의 속성을 명확히 지정해야 합니다.

CartPage.jsx 재확인:

//...
cartItems.map((item) => (
    <div key={item.id}>
        {/* item.name 같이 속성을 정확히 접근해야 합니다. */}
        <span>{item.name}</span>
        <span> | 수량: {item.quantity}개</span>
        <span> | 가격: {item.price * item.quantity}원</span>
    </div>
))
//...

만약 <span>{item}</span> 과 같이 객체 자체를 렌더링하려고 하면 React에서는 오류가 발생하지만, 다른 방식으로 문자열과 더해지는 경우 [object Object]가 출력될 수 있습니다.

이 가이드를 통해 문제가 해결되기를 바랍니다!

 

 

세 번째 문제점 : 장바구니페이지에서 모든 장바구니를 비우는 기능이 작동하지 않음.

미니 키오스크 장바구니 비우기 기능 수정 가이드

안녕하세요. 장바구니 비우기 기능이 작동하지 않는 문제를 해결해 드리겠습니다. 코드에 두 가지 수정이 필요합니다.

1. cartSlice.jsx 리듀서 위치 수정

가장 큰 문제는 cartSlice.jsx 파일에서 remove 리듀서의 위치가 잘못된 것입니다. createSlice를 사용할 때 모든 리듀서는 reducers 객체 안에 정의되어야 합니다.

문제점:
현재 remove 리듀서가 reducers 객체 밖에 선언되어 있어, remove 액션이 제대로 생성되지 않고 리듀서 로직도 스토어에 등록되지 않습니다.

개선된 코드 (cartSlice.jsx):

import { createSlice } from "@reduxjs/toolkit"

const initialState = { cartInfo: [] }

const cartSlice = createSlice({
    name: 'cart',
    initialState,
    // [수정] 모든 리듀서는 reducers 객체 안에 위치해야 합니다.
    reducers: {
        add: (state, action) => {
            const newItem = action.payload
            const existingItem = state.cartInfo.find((item) => item.id == newItem.id);
            if (existingItem) {
                existingItem.stock++;
            } else {
                state.cartInfo.push({...newItem , stock: 1});
            }
        },
        // [수정] remove 리듀서를 reducers 객체 안으로 이동합니다.
        remove: (state) => { 
            state.cartInfo = []; 
        }
    }
})

export default cartSlice.reducer;
export const { add, remove } = cartSlice.actions;

2. CartPage.jsx 총합계 계산 오류 수정

CartPage.jsx에서 장바구니가 비어있을 때 총합계를 계산하는 reduce 함수에서 오류가 발생합니다. 빈 배열에 reduce를 초기값 없이 사용하면 TypeError가 발생하여 앱이 멈출 수 있습니다.

문제점:
cartItems.reduce(...)에 초기값이 없어, cartItems가 빈 배열일 때 에러가 발생합니다.

개선된 코드 (CartPage.jsx):

import { useDispatch, useSelector } from "react-redux";
import { remove } from "../store/cartSlice";

export default function CartPage(props) {
    const dispatch = useDispatch();
    const cartItems = useSelector((state) => state.cart.cartInfo);

    // [수정] reduce 함수에 초기값 0을 추가하여 빈 배열일 때 오류가 발생하지 않도록 합니다.
    const totalPrice = cartItems.reduce((sum, item) => {
        return sum + (item.price * item.stock);
    }, 0); // <--- 초기값 0 추가

    const removeCart = () => { // 불필요한 async 제거
        dispatch(remove());
    };

    return (
        <>
            <h3>장바구니 페이지</h3>
            <div>
                {cartItems.length === 0 ? (
                    <p>장바구니가 비어있습니다.</p>
                ) : (
                    cartItems.map((item) => (
                        <div key={item.id}>
                            {item.name} | 수량 : {item.stock}개 | 가격 : {item.price * item.stock}원
                        </div>
                    ))
                )}
                <hr />
                <h4>총 합계 : {totalPrice.toLocaleString()}원</h4>
                <button type="button" onClick={removeCart}>장바구니 비우기</button>
            </div>
        </>
    );
}

참고: CartPage.jsxbuttontype="button"을 명시해주는 것이 좋습니다. 또한 removeCart 함수에서 불필요한 async 키워드를 제거했습니다.

이 두 가지 사항을 수정하시면 장바구니 비우기 기능과 총합계 계산이 정상적으로 작동할 것입니다.

 

 

* 최종 피드백 : 주석을 부착하여 코드 다시 읽어보기

미니 키오스크 코드 해설 가이드

안녕하세요! 직접 만드신 미니 키오스크의 전체 코드 흐름과 각 요소의 역할을 자세히 이해하실 수 있도록 주석을 포함한 해설 가이드를 준비했습니다. 이 파일이 리액트와 Redux 학습에 도움이 되기를 바랍니다.


프로젝트 구조 및 데이터 흐름

이 키오스크는 다음과 같은 흐름으로 동작합니다.

  1. main.jsx: 앱의 진입점. Redux의 Provider와 Redux-Persist의 PersistGate를 사용해 App 컴포넌트를 감싸서, 앱 전체에서 Redux 상태를 공유하고 영속성을 유지할 수 있게 합니다.
  2. App.jsx: react-router-dom을 사용해 URL 경로에 따라 HomePage, MenuPage, CartPage 중 적절한 페이지를 보여주는 라우팅을 설정합니다.
  3. MenuPage.jsx: 사용자가 '담기' 버튼을 클릭합니다.
  4. useDispatch & add 액션: onClick 이벤트가 발생하면, useDispatch 훅을 통해 cartSlice.js에 정의된 add 액션을 실행시킵니다. 이때 클릭된 메뉴 정보가 액션의 payload로 전달됩니다.
  5. cartSlice.jsx (리듀서): add 액션을 받은 리듀서는 Redux 상태(state)를 업데이트합니다. 장바구니에 이미 상품이 있으면 수량(stock)을 늘리고, 없으면 새로 추가합니다. Redux Toolkit의 Immer.js 덕분에 state.cartInfo.push(...)처럼 직접 상태를 변경하는 것처럼 보이는 코드를 작성할 수 있습니다.
  6. store.jsx & redux-persist: 리듀서에 의해 변경된 상태는 store에 저장됩니다. redux-persist는 이 store의 상태가 변경될 때마다 자동으로 브라우저의 localStorage에 그 내용을 저장합니다.
  7. CartPage.jsx & useSelector: useSelector 훅은 Redux store의 상태 변경을 감지하고, cartInfo 데이터를 가져와 컴포넌트를 다시 렌더링합니다. 결과적으로 사용자는 업데이트된 장바구니 내용을 보게 됩니다. '장바구니 비우기'도 remove 액션을 통해 동일한 흐름으로 동작합니다.

1. store/store.jsx (Redux 스토어 설정)

Redux의 모든 상태(state)를 담는 중앙 저장소인 '스토어'를 만들고, redux-persist를 연결하여 상태를 영속시키는 역할을 합니다.

// Redux Toolkit에서 스토어를 쉽게 설정하게 해주는 configureStore 함수를 가져옵니다.
import { configureStore } from "@reduxjs/toolkit";
// redux-persist 관련 함수들을 가져옵니다.
import { persistReducer, persistStore } from "redux-persist";
// 우리가 만든 cartSlice의 리듀서를 가져옵니다.
import cartSlice from './cartSlice.jsx';
// redux-persist가 상태를 저장할 공간을 지정합니다. 'redux-persist/lib/storage'는 localStorage를 의미합니다.
import storage from 'redux-persist/lib/storage';

// [1] redux-persist 설정 객체를 만듭니다.
const persistConfig = { 
    key: 'cart', // localStorage에 저장될 때 사용될 키 이름입니다. 'persist:cart' 형태로 저장됩니다.
    storage     // 위에서 import한 localStorage를 저장소로 사용하겠다고 명시합니다.
};

// [2] 기존 리듀서에 persist 설정을 적용합니다.
// persistReducer는 cartSlice 리듀서와 persistConfig를 합쳐, 영속성이 적용된 새로운 리듀서를 반환합니다.
const persistedReducer = persistReducer(persistConfig, cartSlice);

// [3] 스토어를 생성합니다.
const store = configureStore({
    // reducer 프로퍼티에 스토어에서 관리할 리듀서들을 등록합니다.
    reducer: {
        // 'cart'라는 이름으로 영속성이 적용된 리듀서를 등록합니다.
        // 이제부터 state.cart 형태로 이 리듀서의 상태에 접근할 수 있습니다.
        cart: persistedReducer
    },
    // middleware 설정: 직렬화할 수 없는 값(non-serializable value)에 대한 경고를 비활성화합니다.
    // redux-persist는 내부적으로 다양한 함수들을 사용하는데, 이들이 직렬화되지 않아 발생하는 기본 경고를 무시하기 위해 추가합니다.
    middleware: (getDefaultMiddleware) =>
        getDefaultMiddleware({
            serializableCheck: false,
        }),
});

// [4] 스토어를 기본으로 내보냅니다. Provider에서 사용됩니다.
export default store;

// [5] 영속성이 적용된 스토어(persistor)를 만들어 내보냅니다. PersistGate에서 사용됩니다.
export const persistor = persistStore(store);

2. store/cartSlice.jsx (상태와 리듀서 정의)

장바구니와 관련된 상태(state)의 초기값과, 그 상태를 어떻게 변경할지에 대한 규칙(reducer)을 정의하는 파일입니다.

// Redux Toolkit의 핵심 기능인 createSlice를 가져옵니다.
import { createSlice } from "@reduxjs/toolkit";

// [1] 이 슬라이스에서 사용할 상태(state)의 초기값을 정의합니다.
// 장바구니 정보는 배열 형태로 관리합니다.
const initialState = { 
    cartInfo: [] 
};

// [2] 슬라이스를 생성합니다.
// 슬라이스는 상태(state), 리듀서(reducers), 액션(actions)을 하나로 묶는 역할을 합니다.
const cartSlice = createSlice({
    name: 'cart',    // 슬라이스의 이름을 지정합니다. 액션 타입의 접두사로 사용됩니다. (예: 'cart/add')
    initialState,    // 이 슬라이스의 초기 상태값입니다.
    reducers: {      // 상태를 어떻게 변경할지를 정의하는 리듀서 함수들의 모음입니다.

        // 'add' 액션이 dispatch 되면 실행될 리듀서 함수입니다.
        // state: 현재의 상태값 (Redux Toolkit의 Immer.js 덕분에 직접 수정 가능)
        // action: dispatch된 액션 객체. action.payload로 전달된 데이터에 접근할 수 있습니다.
        add: (state, action) => {
            // action.payload에는 MenuPage에서 전달한 메뉴 객체가 들어있습니다.
            const newItem = action.payload;

            // 장바구니(state.cartInfo)에 이미 같은 ID의 상품이 있는지 찾습니다.
            const existingItem = state.cartInfo.find((item) => item.id === newItem.id);

            if (existingItem) {
                // 이미 상품이 있다면, 해당 상품의 'stock'(수량)을 1 증가시킵니다.
                existingItem.stock++;
            } else {
                // 장바구니에 없는 새로운 상품이라면, 'stock' 속성을 1로 설정하여 배열에 추가합니다.
                // 스프레드 연산자(...)를 사용해 기존 newItem 객체의 모든 속성을 복사하고 stock 속성을 추가합니다.
                state.cartInfo.push({ ...newItem, stock: 1 });
            }
        },

        // 'remove' 액션이 dispatch 되면 실행될 리듀서 함수입니다.
        remove: (state) => {
            // 장바구니 배열을 빈 배열로 교체하여 모든 상품을 삭제합니다.
            state.cartInfo = [];
        }
    }
});

// [3] 생성된 슬라이스의 리듀서(reducer)를 기본으로 내보냅니다.
// 이 리듀서는 store.jsx에서 스토어를 만들 때 사용됩니다.
export default cartSlice.reducer;

// [4] 생성된 액션 생성자(action creators)들을 named export 합니다.
// 이제 다른 컴포넌트에서 import { add, remove } from './cartSlice' 형태로 가져와 사용할 수 있습니다.
export const { add, remove } = cartSlice.actions;

3. pages/MenuPage.jsx (메뉴 선택 페이지)

사용자가 메뉴를 보고 장바구니에 담는 UI와 기능을 담당합니다.

import { useDispatch } from "react-redux";
import { add } from "../store/cartSlice"; // cartSlice에서 만든 'add' 액션 생성자를 가져옵니다.

export default function MenuPage(props) {
    // useDispatch는 Redux 스토어에 액션을 전달(dispatch)할 수 있게 해주는 훅입니다.
    const dispatch = useDispatch();

    // 샘플 메뉴 데이터입니다. 실제 앱에서는 서버에서 받아오는 데이터가 됩니다.
    const menu = [
        { id: 1, name: "아메리카노", price: 3000 },
        { id: 2, name: "카페라떼", price: 4000 },
        { id: 3, name: "카푸치노", price: 4500 },
    ];

    // 장바구니에 상품을 추가하는 함수입니다.
    const addCart = (clickItem) => {
        // dispatch 함수를 사용해 'add' 액션을 실행합니다.
        // add(clickItem)은 { type: 'cart/add', payload: clickItem } 형태의 액션 객체를 생성합니다.
        dispatch(add(clickItem));
        console.log(clickItem.name + "이(가) 장바구니에 추가되었습니다");
    };

    return (
        <>
            <h3>메뉴 페이지</h3>
            {/* menu 배열을 map 함수로 순회하며 각 메뉴에 대한 버튼을 생성합니다. */}
            {menu.map((m) => (
                // React에서 리스트를 렌더링할 때는 각 요소에 고유한 'key' prop을 제공해야 합니다.
                <div key={m.id}>
                    {/* 
                      onClick 이벤트에 화살표 함수를 사용하는 이유:
                      만약 onClick={addCart(m)} 이라고 쓰면, 컴포넌트가 렌더링될 때 즉시 함수가 실행되어 버립니다.
                      onClick={() => addCart(m)} 처럼 화살표 함수로 감싸주면, 버튼이 '클릭'되었을 때 비로소 addCart(m)이 실행됩니다.
                    */}
                    <button type="button" onClick={() => addCart(m)}>
                        {m.name} ({m.price}원) 담기
                    </button>
                </div>
            ))}
        </>
    );
}

4. pages/CartPage.jsx (장바구니 페이지)

장바구니에 담긴 상품 목록과 총액을 보여주고, 장바구니를 비우는 기능을 제공합니다.

import { useDispatch, useSelector } from "react-redux";
import { remove } from "../store/cartSlice"; // 'remove' 액션 생성자를 가져옵니다.

export default function CartPage(props) {
    // dispatch 함수를 가져옵니다.
    const dispatch = useDispatch();

    // useSelector는 Redux 스토어의 상태(state)를 조회할 수 있게 해주는 훅입니다.
    // state.cart.cartInfo 경로를 통해 스토어의 장바구니 배열 데이터에 접근합니다.
    // 스토어의 상태가 변경되면, 이 컴포넌트는 자동으로 다시 렌더링됩니다.
    const cartItems = useSelector((state) => state.cart.cartInfo);

    // 장바구니에 담긴 상품들의 총 합계를 계산합니다.
    // reduce 함수는 배열의 각 요소를 순회하며 하나의 결과값을 만들어냅니다.
    // sum: 누적값, item: 현재 요소
    // , 0: 초기 누적값. 빈 배열일 때 오류가 나지 않도록 반드시 0을 지정해야 합니다.
    const totalPrice = cartItems.reduce((sum, item) => {
        return sum + (item.price * item.stock);
    }, 0);

    // 장바구니를 비우는 함수입니다.
    const removeCart = () => {
        // 'remove' 액션을 dispatch 합니다. 이 액션은 payload가 필요 없습니다.
        dispatch(remove());
    };

    return (
        <>
            <h3>장바구니 페이지</h3>
            <div>
                {/* 삼항 연산자를 사용해 장바구니가 비어있을 때와 아닐 때 다른 내용을 보여줍니다. */}
                {cartItems.length === 0 ? (
                    <p>장바구니가 비어있습니다.</p>
                ) : (
                    // 장바구니 배열을 map으로 순회하며 각 상품 정보를 화면에 표시합니다.
                    cartItems.map((item) => (
                        <div key={item.id}>
                            {item.name} | 수량 : {item.stock}개 | 가격 : {item.price * item.stock}원
                        </div>
                    ))
                )}
                <hr />
                <h4>총 합계 : {totalPrice.toLocaleString()}원</h4>
                <button type="button" onClick={removeCart}>장바구니 비우기</button>
            </div>
        </>
    );
}

5. App.jsxmain.jsx (앱 구조 및 라우팅)

App.jsx는 페이지 간의 이동(라우팅)을, main.jsx는 앱 전체에 Redux를 연결하는 역할을 합니다.

App.jsx:

import { BrowserRouter, Route, Routes, Link } from "react-router-dom";
import Header from "./실습8/components/Header"; // Header 컴포넌트
import HomePage from "./실습8/pages/HomePage";
import MenuPage from "./실습8/pages/MenuPage";
import CartPage from "./실습8/pages/CartPage";

export default function App(props) {
    return (
        // BrowserRouter: HTML5 History API를 사용해 URL과 UI를 동기화하는 라우터입니다.
        <BrowserRouter>
            <div>
                <Header /> {/* 모든 페이지 상단에 공통으로 표시될 헤더 */}
                {/* Routes: 여러 Route 중 현재 URL과 일치하는 첫 번째 Route 하나만 렌더링합니다. */}
                <Routes>
                    {/* Route: 특정 경로(path)에 어떤 컴포넌트(element)를 보여줄지 정의합니다. */}
                    <Route path="/" element={<HomePage />} />
                    <Route path="/menu" element={<MenuPage />} />
                    <Route path="/cart" element={<CartPage />} />
                </Routes>
            </div>
        </BrowserRouter>
    );
}

main.jsx (예시):

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App'; // App 컴포넌트

// Redux 스토어와 persistor를 가져옵니다.
import store, { persistor } from './example/day06/실습8/store/store.jsx';
// React 앱에 Redux 스토어를 연결하기 위한 Provider를 가져옵니다.
import { Provider } from 'react-redux';
// Redux 상태가 localStorage에서 로드될 때까지 UI 렌더링을 지연시키는 역할을 합니다.
import { PersistGate } from 'redux-persist/integration/react';

ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    {/* Provider로 App 컴포넌트를 감싸고, store를 prop으로 전달합니다. */}
    {/* 이제 App 컴포넌트와 그 모든 자식 컴포넌트들은 Redux 스토어에 접근할 수 있습니다. */}
    <Provider store={store}>
      {/* PersistGate는 Redux 상태가 복원될 때까지 loading prop에 지정된 컴포넌트를 보여줄 수 있습니다. (현재는 null) */}
      <PersistGate loading={null} persistor={persistor}>
        <App />
      </PersistGate>
    </Provider>
  </React.StrictMode>,
);
반응형