React - 디테일,카트 페이지

송용준·2025년 3월 31일
0

기능

  1. Detail에 있는 상품에 주문하기 버튼을 누르면 Cart 화면에 가져와서 뿌려줌
  2. Cart에 이미 상품이 있을 경우 count를 +1

그런데 URL 변경시 새로고침이 되면서 Redux에 state가 날라가는 이슈가 있었다.
그래서 redux-persist 기능으로 선택값을 Local Storage에 저장하여 관리하고 새로고침 버튼을 누르면 state값을 초기화 하는 기능을 구현했다.

Redux 이슈가 해결이 안되서 1주일 동안 고통받음..

Detail화면에 탭 변경,타이머,인풋 등의 useEffect를 사용해서 만드는 기능들도 만들었지만 이건 간단한 기능들이라 기록 안할래
(외부 라이브러리로 import해서 쓰거나 .css 에 클래스 만들어서 import해서 쓰거나 하면 되는듯)

Detail.jsx

import { useParams } from 'react-router-dom'; //라우터로 받은 데이터를 사용하기 위해 불러옴
import "../App.css";  // css 파일
import { useSelector } from 'react-redux';
import { useNavigate } from 'react-router-dom'  // 화면전환할때 쓰는 라우터 라이브러리
import SearchInput from '../components/Input.jsx';
import { SearchList } from '../components/button.jsx';
import { DetailFind } from './DetailFind.jsx';
import { useState  } from 'react';

function DetailPage(){

    let {id} = useParams(); // url로 받은 데이터를 화면에 이동에 따라 동적으로 인식하기 위해 id값을 받아옴
    let shoes = useSelector((state) => state.shoes);    // shoes 값은 Redux로 관리되고 있음
    let 찾은상품 = shoes.find((x) => x.id == id);   // Redux의 id가 라우터로 받은 id와 같은 '찾은상품' 찾기 
    let navigate = useNavigate()  //navigate는 url 이동시 새로고침 안해 그래서 써
    let [value, setValue] = useState(''); //input에 들어갈 value값을 useState로 관리
    let [filteredShoes, setFilteredShoes] = useState(null); //input에 들어갈 value값을 useState로 관리

    return (
        <>
        { 찾은상품 ? 
            (
            // 특정상품 주문하기
            <DetailFind 찾은상품={찾은상품}/>
            ) : (
            <div className="container">
                <h1>* 상품 리스트 *</h1>
                <br></br>
                <SearchInput value={value} setValue={setValue} /> {/* input 밸리데이션 체크후 아래로 value값 리턴 */}
                <SearchList 
                    setFilteredShoes={setFilteredShoes}
                    value={value}
                    shoes={shoes}
                /> {/* 상품 찾기 버튼 */}
                <br/>
                <br/>
                <div className="row">
                    {(filteredShoes && filteredShoes.length === 0) ? (
                        <p>검색된 상품이 없습니다.</p>
                    ) : (
                        (filteredShoes ?? shoes).map((product) => (// ?? : filteredShoes가 null 또는 undefined일 경우 shoes로 대체
                            <div className="col-md-4" key={product.id}>
                                <h4>{product.title}</h4>
                                <p>{product.content}</p>
                                <p>{product.price}</p>
                                {/* <button className="btn btn-danger" onClick={()=>navigate('/detail/'+product.id)} >주문하기</button>  */}
                                <button className="btn btn-danger" onClick={()=>{
                                    navigate('/detail/'+product.id) 
                                }} >주문하기</button> 
                            </div>
                        ))
                    )}
                </div>
            </div>
            )
        }
        </>
    )
}
  
export default DetailPage

DeatailFind.jsx

// Detail 화면에서 상품을 찾았을 때 개별 상품을 보여주는 컴포넌트
import { MoveCart, MoveDetailPageButton, OrderToList } from "../components/button"

export function DetailFind({찾은상품}){ 

    return (
        <div className="container">
            <div className="row">

                <div className="col-md-6">
                    {/* <img src={'https://codingapple1.github.io/shop/shoes'+(찾은상품.id+1)+'.jpg'} width="100%" /> */}
                </div>

                <div className="col-md-6">
                    <h2>찾은 상품</h2>
                    <h4 className="pt-5">{찾은상품.title}</h4>
                    <p>{찾은상품.content}</p>
                    <p>{찾은상품.price}</p>
                    <OrderToList item={{ id: 찾은상품.id, name: 찾은상품.title }} />
                </div>
                        
                {/* 디테일 All 페이지로 이동버튼 */}
                {<MoveDetailPageButton/>}
                <br/>
                
                {/* 장바구니로 이동버튼 */}
                {<MoveCart/>}

            </div>
        </div> 
    )
}

Cart.jsx

import { Table } from 'react-bootstrap';
import { useSelector, useDispatch } from 'react-redux'; //context api 보다 설정이 복잡하지만 성능 최적화 가능
import { addCount, addItem } from '../store.js';

function Cart() { 
  // Redux store에서 cart 상태를 가져옴
  let cartList = useSelector((state) => state.cart); // Redux store에서 cart 상태 가져오기
  let shoes = useSelector((state) => state.shoes); // Redux store에서 shoes 상태 가져오기
  let dispatch = useDispatch(); //전달함수
    
  //행추가
  const addNewItem = () => {
    const newItem = {id: Date.now(), name:'Song', count:1}
    console.log('행추가 == '+JSON.stringify(newItem))
    // const newItem = {id: state.cart.length, name:Name, count:1} --> 입력값 받아서 저장하는 걸로 구현해보기
    dispatch(addItem(newItem));
  }

  return (
    <div>
      <Table>
        <thead>
          <tr>
            <th>상품번호</th>
            <th>이름</th>
            <th>수량</th>
          </tr>
        </thead>

        <tbody>
        {
          !cartList.cart ? (
            <tr>
              <td colSpan="5">값없음</td>
            </tr>
          ) : (
            cartList.cart.map((a, i) => (
              <tr key={i}>
                <td>{a.id}</td>
                <td>{a.name}</td>
                <td>{a.count}</td>
                <td>
                  <button onClick={() => dispatch(addCount(a.id))}>+</button>
                </td>
              </tr>
            ))
          )
        }
        </tbody>
      </Table>
      
      <button onClick={addNewItem}>행추가</button>

    </div>
  )
    
}    
export default Cart;

Cart.jsx 에선 Detail.jsx에서 가져온 상품값을 map으로 뿌림

store.js

// Redux 라이브러리 사용 - state를 중앙에서 관리
import { configureStore } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist'; // redux-persist 관련 import
import { persistedCartReducer } from './cartSlice.js';
import shoesReducer from './shoesSlice.js'
import { persistedAuthReducer } from './authSlice.js';
 
// 🟢 Store 생성
const store = configureStore({
  reducer: {
    cart: persistedCartReducer, // cart는  default 가 아닌 이름으로 보냈기에 작명한 그대로 써야함
    shoes: shoesReducer,        // shoes는 default 로 보냈기에 {} 안에 쓰지않고 이름을 마음대로 지을수 있음  
    auth : persistedAuthReducer, 
  },
});

// 🟢 persistor 생성
export const persistor = persistStore(store);
export default store;

cartSlice.js

import { configureStore, createSlice } from '@reduxjs/toolkit';
import { persistStore, persistReducer } from 'redux-persist'; // redux-persist 관련 import
import storage from 'redux-persist/lib/storage'; // 로컬 스토리지 사용

// 🟢 Cart Slice
let cartSlice = createSlice({
    name: 'cart',
    initialState: {
      cart: []  // cart를 배열로 감싸서 초기화
    },
    reducers: {
  
      // cart에 수량 추가
      addCount(state, action) {
        let 번호 = state.cart.findIndex((a) => a.id === action.payload);
        if (번호 !== -1) {  //값이 없을시 index 값으로 -1 반환
          state.cart[번호].count++;
        }
      },
  
      // cart에 수량 감소
      deleteCount(state, action) {
        let 번호 = state.cart.findIndex((a) => a.id === action.payload);
        if (번호 !== -1) {  //값이 없을시 index 값으로 -1 반환
          state.cart[번호].count--;
        }
      },
  
      // 선택한 상품 Cart에 추가
      addItem(state, action) {
        let found = state.cart.find((item) => item.id === action.payload.id);
        if (found) {  // 같은 상품이 있을 경우 수량 증가
          found.count++; 
        } else {  // 같은 상품이 없을경우 상품 추가
          state.cart.push({ ...action.payload, count: 1 });
          console.log('추가')
        }
      },
  
      // 선택한 상품 Cart에서 삭제
      deleteItem(state, action) {
        let cartIndex = state.cart.findIndex((a) => a.id === action.payload);
        if (cartIndex !== -1) {
          state.cart.splice(cartIndex, 1);  // 해당 index의 요소 삭제
          console.log('삭제 완료');
        }
      },
  
      // cart 초기화
      clear(state){
        state.cart = []; // cart를 빈 배열로 초기화
      },
    },
  });

  // 🟢 persistConfig 설정
const persistConfig = {
    key: 'cart', // localStorage에서 저장할 key 값
    storage, // 로컬 스토리지 사용
    whitelist: ['cart'], // 영속화할 상태 목록 (cart만 저장)
  };

  export const persistedCartReducer = persistReducer(persistConfig, cartSlice.reducer);
  export const { addCount, deleteCount, addItem, deleteItem, clear } = cartSlice.actions;

main.jsx

import React from 'react';
import ReactDOM from 'react-dom/client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; //실시간 데이터인 react-query
import { Provider } from 'react-redux';   //Redux
import { PersistGate } from 'redux-persist/integration/react';  //Redux 영속성
import { BrowserRouter } from 'react-router-dom'; //라우터
import store from './store/store';
import { persistor } from './store/store';
import App from './App';

const queryClient = new QueryClient();
const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
  <QueryClientProvider client={queryClient}>    
    <Provider store={store}>
      <PersistGate loading={null} persistor={persistor}>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </PersistGate>
    </Provider>
  </QueryClientProvider>
);

크롬 확장 프로그램

우선 크롬 확장프로그램 Redux DevTools 이거 다운받기
https://chromewebstore.google.com/detail/redux-devtools/lmhkpmbekcpmknklioeibfkpmmfibljd?hl=ko

F12>Redux>prefetch 에 state가 어떻게 관리되는지 나옴

Redux 이슈

shoes 값은 마운트 되면서 axios로 가지고 오기 때문에 언제나 redux 안에서 관리하지만
cart 값은 화면에서 내가 선택한 값이기에 따로 저장할 방식(redux-persist)과 공간(Local Storage)이 필요하다.

가장 시간을 많이 잡아먹은 이슈 내용은 store.js 로 넘어온 state.cart 값이 객체라는 것이다. map으로 뿌려줄 거기 때문에 배열로 바꿔야 한다.

etc

배열 구조 분해 할당

let [likeCount,likeCountFunc] = useLike()

요게 처음에 뭔가 싶었는데 '배열 구조 분해 할당' 라고 함
useLike() 함수의 return 값을 배열로 리턴함 그걸 배열로 받아서 선언하는것
이래 놓고 likeCount, likeCountFunc 그냥 쓰면 됨

널 병합 연산자

(filteredShoes ?? shoes).map((product) => (

요론 문법도 있다
?? : filteredShoes가 null 또는 undefined일 경우 shoes로 대체

사용기술

  • redux
  • redux-persist
  • Router

profile
용용

0개의 댓글