ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 아마존 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>
        }

     

     

     

     

    다음과 같이 데이터가 백엔드에서 온 것을 알 수 있고, 

     

     

    리덕스를 보면 디테일 리퀘스트도 성공하고 

     

    리스트를 가져오는데도 성공한 것을 확인할 수 있다. 

     

     

Designed by Tistory.