> > React Shopping Tutorial
위의 영상을 복습하는 글이다.
index.tsx
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; import {QueryClient, QueryClientProvider} from 'react-query'; const client = new QueryClient(); ReactDOM.render( <QueryClientProvider client={client}> <App /> </QueryClientProvider> ,document.getElementById('root'));
App.tsx
import {useState} from 'react' import {useQuery} from 'react-query' import Item from './Item/Item'; * 받아온 데이터에 있는 하나의 상품 Component import Grid from '@material-ui/core/Grid'; * 컨텐츠를 반응형 격자로 배치시켜야 하는 경우 유용합니다. * Material UI는 기본적으로 12열 격자(grid) 시스템을 가지고 있으며, 각 브레이크포인트(breakpoint) 별로 각 셀이 몇 열을 차지할 것인지를 명시해줄 수 있습니다. import AddShoppingCartIcon from '@material-ui/icons/AddShoppingCart'; * Side Navigation을 열어줄 카트모양의 아이콘 import Badge from '@material-ui/core/Badge'; * Cart에 담긴 수량을 알려주는 Badge import {Wrapper, StyledButton} from './App.styles'; export type CartItemType = { id: number; category: string; description: string; image: string; price: number; title: string; amount: number; } * Typescript의 type 선언 CartItemType형 데이터는 아래의 구성요소를 갖고 있어야 한다. const getProducts = async (): Promise<CartItemType[]> => await(await >fetch('https://fakestoreapi.com/products')).json(); * 비동기화 함수인 async를 이용해서 API로 부터 데이터를 받아온다. * await가 두개인 이유는 json() 때문이다. * Promise는 프로미스가 생성될 때 꼭 알 수 있지는 않은 값을 위한 대리자로, 비동기 연산이 종료된 이후의 결과값이나 실패 이유를 처리하기 위한 처리기를 연결할 수 있도록 한다. 프로미스를 사용하면 비동기 메서드에서 마치 동기 메서드처럼 값을 반환할 수 있다. const App = () => { const [cartOpen, setCartOpen] = useState(false); * Cart가 열려있는지 닫혀있는지 확인 할 수 있는 state를 useState로 관리한다. const [cartItems, setCartItems] = useState([] as CartItemType[]) * 받아온 모든 상품을 관리하는 state이다. const {data, isLoading, error} = useQuery<CartItemType[]>( 'products', getProducts ); * useQuery<type>('key', function) => type은 받아온 데이터의 타입을 의미한다. * data는 getProducts로 받아온 json형태의 자료를 의미한다. console.log(data); const getTotalItems = (items: CartItemType[]) => items.reduce((ack:number, item) => ack + item.amount, 0) * reduce(ack, cur, idx, src(원본배열)) * reduce(function, 초기값) * reduce() 메서드는 배열의 각 요소에 대해 주어진 리듀서(reducer) 함수를 실행하고, 하나의 결과값을 반환합니다. const handleAddToCart = (clickedItem: CartItemType) => { setCartItems(prev => { * useState에서 set함수를 이용할 때 첫번째 인자는 해당 state이다. const isItemInCart = prev.find(item => item.id === clickedItem.id) if(isItemInCart) { return prev.map(item => ( item.id === clickedItem.id ? {...item, amount: item.amount + 1} : item )); } return [...prev, {...clickedItem, amount: 1}]; }); }; * Cart에 새로운 상품을 추가하는 함수이다. * 1.Cart에 같은 상품이 있는지 확인한다. * 2.만약에 있다면 그 상품의 amount를 1 증가시키고 아니라면 amount를 1로 만든다 const handleRemoveFromCart = (id: number) => { setCartItems(prev => prev.reduce((ack, item) => { if(item.id === id){ if(item.amount === 1) return ack; return [...ack, {...item, amount: item.amount - 1}]; } else{ return [...ack, item]; } }, [] as CartItemType[]) ); }; * Cart에서 상품을 삭제하는 함수이다. * 1.인자로 준 id와 일치하는 상품이 있는지 확인한다 * 1-1 id와 일치하는 상품의 수량이 1인지 확인한다 * 1-1-1 있다면 CartItems 배열에서 삭제한다 * 1-1-2 없다면 그 상품의 수량은 2개 이상이고 수량을 -1 한다. * 1-2 없다면 [...ack, item]을 반환한다. if(isLoading) return <LinearProgress /> * 로딩하는 동안 나오는 화면이다. if (error) return <div>Something is wrong...</div> * error 예외처리 return ( <Wrapper> <Drawer anchor='right' open={cartOpen} onClose={() => setCartOpen(false)}> * anchor : Drawer의 위치 * open : open시 실행할 함수 <Cart cartItems={cartItems} addToCart={handleAddToCart} removeFromCart={handleRemoveFromCart}/> * cartItems : Cart속에 있는 상품들이다. </Drawer> <StyledButton onClick={() => setCartOpen(true)}> <Badge badgeContent={getTotalItems(cartItems)} color='primary'> * Badge Component는 child의 top-right에 작은 뱃지를 만든다. * badgeContent : badge안에 들어갈 내용을 의미한다. <AddShoppingCartIcon /> </Badge> </StyledButton> <Grid container spacing={3}> {data?.map((item => ( <Grid item key={item.id} xs={12} sm={4}> <Item item={item} handleAddToCart={handleAddToCart} /> </Grid> )))} </Grid> * Container와 Item이라는 두 가지 개념으로 구분되어 있다. Container는 Items를 감싸는 부모 요소이며, 그 안에서 각 Item을 배치할 수 있다. * spacing : spacing = spacing * 8px, e.g. spacing={2} creates a 16px wide gap. * lg, md, sm, xl, xs 속성에 1에서 12 사이의 정수를 입력하면 전체의 ((입력한 값)/12) % 만큼 너비를 차지하게 된다. </Wrapper> ); } export default App;
getProuct의 결과(CartItemType[])
item (CartItemType)
Item.tsx
import Button from '@material-ui/core/Button'; import {CartItemType} from '../App'; import {Wrapper} from './Item.styles'; type Props = { item: CartItemType; handleAddToCart: (clickedItem: CartItemType) => void; } * Props의 type을 정의한다. const Item: React.FC<Props> = ({item, handleAddToCart}) => { return( <Wrapper> <img src={item.image} alt={item.title} /> <div> <h3>{item.title}</h3> <p>{item.description}</p> <h3>${item.price}</h3> </div> <Button onClick={() => handleAddToCart(item)}>Add to cart</Button> </Wrapper> ); * React.FC<Props> : Item함수의 반환형은 react component이고 props의 type은 Props이다. }; export default Item;
Cart.tsx
import CartItem from '../CartItem/CartItem'; import {Wrapper} from './Cart.styles'; import {CartItemType} from '../App'; type Props = { cartItems: CartItemType[]; addToCart: (clickedItem: CartItemType) => void; // Function : addTocart, return value : void removeFromCart: (id: number) => void; }; * 넘겨받을 props의 타입을 정의한다. * addToCart, removeFromCart는 기능만 갖고 있으므로 반환값이 void이다. const Cart: React.FC<Props> = ({cartItems, addToCart, removeFromCart}) => { const calculateTotal = (items: CartItemType[]) => items.reduce((ack: number, item) => ack + item.amount * item.price, 0) * amount와 price를 곱해서 총 얼마인지 계산한다. return ( <Wrapper> <h2>Your Shopping Cart</h2> {cartItems.length === 0 ? <p>No items in cart.</p> : null} {cartItems.map(item => ( <CartItem key={item.id} item={item} addToCart={addToCart} removeFromCart={removeFromCart} /> ))} <h2>Total: ${calculateTotal(cartItems).toFixed(2)}</h2> * toFixed() : 소수점 설정 </Wrapper> ); }; export default Cart;
CartItem.tsx
import Button from '@material-ui/core/Button' import {CartItemType} from '../App'; import {Wrapper} from './CartItem.styles'; type Props = { item: CartItemType; addToCart: (clickedItem: CartItemType) => void; removeFromCart: (id: number) => void; } const CartItem: React.FC<Props> = ({item, addToCart, removeFromCart}) => ( <Wrapper> <div> <h3>{item.title}</h3> <div className="information"> <p>Price: ${item.price}</p> <p>Total: ${(item.amount * item.price).toFixed(2)}</p> </div> <div className="buttons"> <Button size="small" disableElevation variant="contained" onClick={() => removeFromCart(item.id)} > - </Button> <p>{item.amount}</p> <Button size="small" disableElevation variant="contained" onClick={() => addToCart(item)} > + </Button> </div> </div> <img src={item.image} alt={item.title} /> </Wrapper> ); export default CartItem;