React

React 장바구니 기능 구현

martinooo 2023. 3. 9. 14:11


리액트 & 타입스크립트 활용해서 장바구니 기능 구현했다.

웹/앱 개발에서 중요한 CRUD 기능 및 useReducer 활용해서 생산성이 좋게 설계해서 만듬.

기본적인 프로그래밍에 개념과 가장 기본적이 hooks 활용에 대해서 깊이 공부할 수 있다. 

javascript filter, reduce, map, switch 활용했다.

 

 

useReducer 미리 숙지하고 하면 유용하게 사용할 수 있다. 

  • useReducer: useState처럼 state생성하고 관리하는 hook 이다. 
  • useReducer 잘사용하려면 3가지를 알아두면 좋다. 
    • Reducer, Dispatch, Action 
  • Reducer: State 상태를 업로드하는 역할 
    Dispatch: Reducer 요구를 하는 역할
    Action: 어떤 요구를 보내는 역할
  • Dispatch > Action > Reducer 
  • dispatch 객체 형태로 인자를 전달해서 recuder에서 예외처리를 해서 원하는
    값을 파싱할 수 있다. 


 

Github Source ☝ 구현 소스를 보실수 있습니다. (cart 폴더)

https://github.com/yoogukhyeon/react-camp

 

GitHub - yoogukhyeon/react-camp

Contribute to yoogukhyeon/react-camp development by creating an account on GitHub.

github.com

 


 

import React, { useState, useContext, useReducer, useEffect, ReactNode } from 'react';
import cartItems from './data';
import reducer from './reducer';
// ATTENTION!!!!!!!!!!
// I SWITCHED TO PERMANENT DOMAIN
const url = 'https://course-api.com/react-useReducer-cart-project';

interface IProps {
	children: ReactNode;
}

interface Cart {
	id: number;
	img: string;
	title: string;
	price: number;
	amount: number;
}

interface InitialState {
	loading: boolean;
	cart: Cart[];
	total: number;
	amount: number;
	clearCart?: () => void;
	remove?: ((key: number) => void) | any;
	toggleAmount?: ((key: string | number) => void) | any;
}

//초기값 셋팅
const initialState: InitialState = {
	loading: false,
	cart: [],
	total: 0,
	amount: 0,
};

const AppContext = React.createContext(initialState);

const AppProvider = ({ children }: IProps) => {
	//useReducer로 새로운 state 생성
	//useState처럼 첫번째는 state 가진다.
	//두번쨰 요소는 useReducer가 만들어준 dispatch가 들어있다.
	//useReducer 인자를 2가지를 받는다. 두번째는 state 들어갈 초기값을 받는다.
	const [state, dispatch] = useReducer(reducer, initialState);

	const clearCart = () => {
		dispatch({ type: 'CLEAR_CART' });
	};
	const remove = (id: number) => {
		dispatch({ type: 'REMOVE', payload: id });
	};

	const toggleAmount = (id: number, type: string) => {
		dispatch({ type: 'TOGGLE_AMOUNT', payload: { id, type } });
	};

	//state 변경될때마다 dispatch 실행
	useEffect(() => {
		dispatch({ type: 'GET_TOTAL' });
	}, [state.cart]);

	const fetchData = async () => {
		dispatch({ type: 'LOADING' });
		const response = await fetch(url);
		const cart = await response.json();
		dispatch({ type: 'DISPLAY_ITEMS', payload: cart });
	};

	useEffect(() => {
		fetchData();
	}, []);

	return (
		<AppContext.Provider
			value={{
				...state,
				clearCart,
				remove,
				toggleAmount,
			}}
		>
			{children}
		</AppContext.Provider>
	);
};

// 전역으로 쉽게 사용하기위해서 선언
export const useGlobalContext = () => {
	return useContext(AppContext);
};

export { AppContext, AppProvider };

 

//reducer 함수는 2가지 인자를 받는다.
//여기서 예외처리를 해서 state 원하는 값을 만들수있다.
//이벤트가 일어날때 dispatch로 값을 전달해서 action활용하면 된다.
const reducer = (state: any, action: any) => {
	switch (action.type) {
		case 'CLEAR_CART':
			// 초기화
			return { ...state, cart: [] };
		case 'REMOVE':
			//filter 함수로 cart id화 playload id 맞지 않는것들만 새로운 배열로 반환 받는다.
			return { ...state, cart: state.cart.filter((item: any) => item.id !== action.payload) };
		case 'GET_TOTAL':
			//reduce 함수를 활용해서 total 개수와 값을 구한다.
			let { total, amount } = state.cart.reduce(
				// 첫번째 인자는 누적값, 두번째 인자는 현잿값
				(total: any, item: any) => {
					const { price, amount } = item;
					//누적값과 현재 값을 곱해서 총값을 구한다
					const itemTotal = price * amount;
					total.total += itemTotal; //total.total = total.total + itemTotal 같은뜻
					total.amount += amount; // total.amount = total.amount + amount 같은뜻
					return total;
				},
				//초기값 설정
				{
					total: 0,
					amount: 0,
				}
			);
			total = parseFloat(total.toFixed(2));
			return { ...state, total, amount };
		case 'LOADING':
			return { ...state, loading: true };
		case 'DISPLAY_ITEMS':
			return { ...state, cart: action.payload, loading: false };
		case 'TOGGLE_AMOUNT':
			let tempCart = state.cart
				.map((val: any) => {
					if (val.id === action.payload.id) {
						if (action.payload.type === 'inc') {
							return { ...val, amount: val.amount + 1 };
						} else {
							return { ...val, amount: val.amount - 1 };
						}
					}

					return val;
				})
				.filter((item: any) => item.amount !== 0);
			return { ...state, cart: tempCart };
	}
	throw new Error('no action type');
};

export default reducer;