-
아마존 E-commerce 클론 -7) HomeScreen, ProductScreen에 리덕스 추가하기NODE.JS 2021. 5. 26. 21:49
프론트엔드로 가서 npm install redux react-redux를 해준다.
리덕스 원리 가장 기본적인 리덕스를 이용해보자.
initialState와 reduer를 만들어주고,
둘을 이용해서 리덕스 스토어를 생성한다.
이 리덕스 스토어는 단순히 product 데이터만 돌려보내줄 것이다.
src>store.js
import data from "./data"; import{createStore} from 'redux'; const initialState = {}; const reducer = (state, action)=>{ return {products:data.products}; } const store = createStore(reducer, initialState); export default store;
우리 앱에 리덕스를 추가하자.
index.js를 다음과 같이 provider로 감싸준다.
import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import store from './store'; import {Provider} from 'react-redux'; ReactDOM.render( <Provider store={store}> <React.StrictMode> <App /> </React.StrictMode> </Provider> , document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals();
npm install redux-thunk해준다.
스토어에도 redux-thunk를 추가해주자.
import{createStore, compose, applyMiddleware} from 'redux'; import thunk from 'redux-thunk' import data from "./data"; const initialState = {}; const reducer = (state, action)=>{ return {products:data.products}; } const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||compose; const store = createStore(reducer, initialState, composeEnhancer(applyMiddleware(thunk))); export default store;
리덕스를 보면 시작된 것을 확인할 수 있다.
보내준대로 products가 담겨있다. 홈스크린으로 가서 ajax를 보내는 대신에 reduxstore를 이용해보자.
먼저 src>constants>productConstants.js파일을 만들어준다.
export const PRODUCT_LIST_REQUEST = 'PRODUCT_LIST_REQUEST'; export const PRODUCT_LIST_SUCCESS = 'PRODUCT_LIST_SUCCESS'; export const PRODUCT_LIST_FAIL = 'PRODUCT_LIST_FAIL';
그다음 src>actions>productActions.js
파일로 가서 작성해준다.
(homescreen에서 axios를 보냈던 부분을 여기서 해준다. )
import { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } from "../constants/productConstants" import Axios from 'axios' export const listProducts = () =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get('/api/products'); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } }
src>reducers>productReducers.js파일을 다음과 같이 케이스로 정리해준다.
여기서 맨 처음 products상태를 []로 지정해줘야 한다.
const { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } = require('../constants/productConstants'); export const productListReducer = (state = {products: []}, action)=>{ switch(action.type){ case PRODUCT_LIST_REQUEST: return {loading: true}; case PRODUCT_LIST_SUCCESS: return {loading:false, products:action.payload}; case PRODUCT_LIST_FAIL: return {loading:false, error:action.payload}; default: return state; } }
다시 store.js파일로가서
다음과 같이 리듀서를 combinReducers로 바꿔준다.
import{combineReducers, createStore, compose, applyMiddleware} from 'redux'; import thunk from 'redux-thunk' import { productListReducer } from './reducers/productReducers'; const initialState = {}; const reducer = combineReducers({ productList: productListReducer }) const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__||compose; const store = createStore(reducer, initialState, composeEnhancer(applyMiddleware(thunk))); export default store;
홈스크린을 다음과 같이 리덕스를 이용한 것으로 변경해준다.
import React , {useEffect} from 'react' import Product from '../components/Product' import LoadingBox from '../components/LoadingBox'; import MessageBox from '../components/MessageBox'; import {useDispatch, useSelector} from 'react-redux' import { listProducts } from '../actions/productActions'; function HomeScreen() { const dispatch = useDispatch(); const productList = useSelector(state=>state.productList); const {loading, error, products} = productList; useEffect(() => { dispatch(listProducts()); }, [])
다음과 같이 state를 확인할 수 있다.
여기서 더 에러를 방지하기 위해서는 productReducer에가서 다음과 같이 loading:true를 추가해주면 된다.
const { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } = require('../constants/productConstants'); export const productListReducer = (state = { loading:true, products: []}, action)=>{ switch(action.type){ case PRODUCT_LIST_REQUEST: return {loading: true}; case PRODUCT_LIST_SUCCESS: return {loading:false, products:action.payload}; case PRODUCT_LIST_FAIL: return {loading:false, error:action.payload}; default: return state; } }
상품 디테일 페이지에도 redux를 이용해보자.
constants에 다음과 같이 디테일을 추가하자.
export const PRODUCT_LIST_REQUEST = 'PRODUCT_LIST_REQUEST'; export const PRODUCT_LIST_SUCCESS = 'PRODUCT_LIST_SUCCESS'; export const PRODUCT_LIST_FAIL = 'PRODUCT_LIST_FAIL'; export const PRODUCT_DETAILS_REQUEST = 'PRODUCT_DETAILS_REQUEST'; export const PRODUCT_DETAILS_SUCCESS = 'PRODUCT_DETAILS_SUCCESS'; export const PRODUCT_DETAILS_FAIL = 'PRODUCT_DETAILS_FAIL';
actions로 가서 다음과 같이 액션을 추가하자. 백엔드에서 정보를 가져올 것이다.
payload로는 상품아이디를 같이 보낸다.
만약 에러가 발생한다면 에러메세지를 보여주는데, 에러응답이 있고 메세지가 있다면 그것을 보여주고, 없으면 general 메세지를 보여준다.
import { PRODUCT_DETAILS_FAIL, PRODUCT_DETAILS_REQUEST, PRODUCT_DETAILS_SUCCESS, PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS } from "../constants/productConstants" import Axios from 'axios' export const listProducts = () =>async(dispatch)=>{ dispatch({ type:PRODUCT_LIST_REQUEST }); try{ const {data} = await Axios.get('/api/products'); dispatch({type:PRODUCT_LIST_SUCCESS, payload:data}); }catch(error){ dispatch({type:PRODUCT_LIST_FAIL, payload:error.message}); } } export const detailsProduct = (productId) =>async(dispatch)=>{ dispatch({ type:PRODUCT_DETAILS_REQUEST, payload:productId }); try{ const {data} = await Axios.get(`/api/products/${productId}`); dispatch({type:PRODUCT_DETAILS_SUCCESS, payload:data}) }catch(error){ dispatch({type:PRODUCT_DETAILS_FAIL, payload:error.response && error.response.data.message? error.response.data.message:error.message}) } }
리듀서로 가서 다음과 같이 케이스 작성해준다.
const { PRODUCT_LIST_FAIL, PRODUCT_LIST_REQUEST, PRODUCT_LIST_SUCCESS, PRODUCT_DETAILS_REQUEST, PRODUCT_DETAILS_SUCCESS, PRODUCT_DETAILS_FAIL } = require('../constants/productConstants'); export const productDetailsReducer = (state= {product:{}, loading:true}, action)=>{ switch(action.type){ case PRODUCT_DETAILS_REQUEST: return {loading:true} case PRODUCT_DETAILS_SUCCESS: return {loading:false, product:action.payload} case PRODUCT_DETAILS_FAIL: return {loading:false, error:action.payload} default: return state; } }
store.js로 가서 combineReducer에 추가해준다.
import { productDetailsReducer, productListReducer } from './reducers/productReducers'; const initialState = {}; const reducer = combineReducers({ productList: productListReducer, productDetails : productDetailsReducer })
ProductScreen.js로 가서 수정해주자.
import React from 'react' import Rating from '../components/Rating' import data from '../data' import {Link} from 'react-router-dom'; import {useSelector} from 'react-redux' function ProductScreen(props) { const productDetails=useSelector(state => state.productDetails) const {loading, error, product} = productDetails;
여기서 homeScreen의 loading파트를 복사해서 다시 가져온다.
import React from 'react' import Rating from '../components/Rating' import {Link} from 'react-router-dom'; import {useSelector} from 'react-redux' import LoadingBox from '../components/LoadingBox'; import MessageBox from '../components/MessageBox'; function ProductScreen(props) { const productDetails=useSelector(state => state.productDetails) const {loading, error, product} = productDetails; if(!product){ return <div>Product Not Found</div> } return ( <div> {loading ?( <LoadingBox></LoadingBox> ): error?( <MessageBox variant="danger">{error}</MessageBox> ): ( <div> <Link to ="/">Back to result</Link> <div className="row top"> <div className="col-2"> <img className="large"src={product.image} alt={product.name} /> </div> <div className="col-1"> <ul> <li> <h1>{product.name}</h1> </li> <li> <Rating rating={product.rating} numReviews={product.numReviews}/> </li> <li> Price : ${product.price} </li> <li> Description: <p>{product.description}</p> </li> </ul> </div> <div className="col-1"> <div className="card card-body"> <ul> <li> <div className="row"> <div>Price</div> <div className="price">${product.price}</div> </div> </li> <li> <div className="row"> <div>Status</div> <div className="status">{product.countInStock>0?( <span className="success">In Stock</span>) : (<span className="danger">Unavailable</span>) }</div> </div> </li> <li> <button className="primary block"> Add to Cart </button> </li> </ul> </div> </div> </div> </div> )} </div> ) } export default ProductScreen
backend의 서버.js파일로 가서 루트 생성해준다.
app.get('/api/products/:id' ,(req, res)=>{ const product = data.products.find(x=>x._id===req.params.id) if(product){ res.send(product); }else{ res.status(404).send({message:'Product not Found'}) } })
product스크린을 usedispatch를 이용해서 다음과 같이 수정해준다.
import React,{useEffect} from 'react' import Rating from '../components/Rating' import {Link} from 'react-router-dom'; import {useDispatch, useSelector} from 'react-redux' import LoadingBox from '../components/LoadingBox'; import MessageBox from '../components/MessageBox'; import { detailsProduct } from '../actions/productActions'; function ProductScreen(props) { const dispatch = useDispatch(); const productId = props.match.params.id; const productDetails=useSelector(state => state.productDetails) const {loading, error, product} = productDetails; useEffect(() => { dispatch(detailsProduct(productId)); }, [dispatch, productId]); if(!product){ return <div>Product Not Found</div> }
다음과 같이 데이터가 백엔드에서 온 것을 알 수 있고, 리덕스를 보면 디테일 리퀘스트도 성공하고 리스트를 가져오는데도 성공한 것을 확인할 수 있다. 'NODE.JS' 카테고리의 다른 글
아마존 E-commerce 클론 -9) CartScreen 만들고, removeFromCart버튼 활성화하기 (0) 2021.05.27 아마존 E-commerce 클론 -8) add to Cart 버튼 기능 만들고 redux store에 카트정보 넣어서 활용하기 (0) 2021.05.27 아마존 E-commerce 클론 -6) Node.js server 만들기 (0) 2021.05.26 아마존 E-commerce 클론 -5) Node.js server 만들기 (0) 2021.05.26 아마존 E-commerce 클론 -4) 상품 디테일 페이지 만들기 (0) 2021.05.26