그런데 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가 어떻게 관리되는지 나옴
shoes 값은 마운트 되면서 axios로 가지고 오기 때문에 언제나 redux 안에서 관리하지만
cart 값은 화면에서 내가 선택한 값이기에 따로 저장할 방식(redux-persist)과 공간(Local Storage)이 필요하다.
가장 시간을 많이 잡아먹은 이슈 내용은 store.js 로 넘어온 state.cart 값이 객체라는 것이다. map으로 뿌려줄 거기 때문에 배열로 바꿔야 한다.
let [likeCount,likeCountFunc] = useLike()
요게 처음에 뭔가 싶었는데 '배열 구조 분해 할당' 라고 함
useLike() 함수의 return 값을 배열로 리턴함 그걸 배열로 받아서 선언하는것
이래 놓고 likeCount, likeCountFunc 그냥 쓰면 됨
(filteredShoes ?? shoes).map((product) => (
요론 문법도 있다
?? : filteredShoes가 null 또는 undefined일 경우 shoes로 대체