230614 - React(meals, router)

๋ฐฑ์Šน์—ฐยท2023๋…„ 6์›” 14์ผ
1

๐Ÿšฉ React

์Œ์‹ ์ฃผ๋ฌธ ์‚ฌ์ดํŠธ ์ฝ”๋”ฉ(๊ธฐ์กด ๋‚ด์šฉ ์ถ”๊ฐ€/์ˆ˜์ •) 3์ผ์ฐจ

๐Ÿ“ ์„ค๋ช…

1์ผ์ฐจ

  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์Œ์‹์„ ํ•œ ๊ฐœ์”ฉ ์ถ”๊ฐ€ํ•˜๊ณ  ์ œ๊ฑฐํ•  ์ˆ˜ ์žˆ๊ฒŒ ์ฝ”๋”ฉ
  • ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ์Œ์‹์„ ์ถ”๊ฐ€/์ œ๊ฑฐ ํ•  ๋•Œ๋งˆ๋‹ค ๊ธˆ์•ก ๊ณ„์‚ฐ
  • css module์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•œ ์ปดํฌ๋„ŒํŠธ์— ๋ชจ๋“ˆ ํ•œ ๊ฐœ์”ฉ ์ƒ์„ฑํ•˜์—ฌ ์ž‘์—…
  • firebase์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋‚ด์šฉ ์ถœ๋ ฅ

2์ผ์ฐจ

  • input ์ž…๋ ฅ์ฐฝ ์œ ํšจ์„ฑ๊ฒ€์‚ฌ

3์ผ์ฐจ



โœ’๏ธ ์ฝ”๋“œ ์ž‘์„ฑ

  • ์ปดํฌ๋„ŒํŠธ๋งˆ๋‹ค css module ๋”ฐ๋กœ ์žˆ๊ธฐ ๋•Œ๋ฌธ์— css๋Š” ์ œ์™ธํ•˜๊ณ  ์ปดํฌ๋„ŒํŠธ๋งŒ ์ž‘์„ฑ

์ž…๋ ฅ

App.js

import { useState } from 'react';
import Cart from './components/Cart/Cart';
import Header from './components/Layout/Header';
import Meals from './components/Meals/Meals';
import CartProvider from './store/CartProvider';
import './App.css';

function App() {
  const [cartInShow, setCartInShow] = useState(false);
  //cart๋ชจ๋‹ฌ์ฐฝ์ด ๋ณด์ด๊ณ  ์•ˆ๋ณด์ด๊ณ , ์ƒํƒœ๊ด€๋ฆฌ

  //cart๋ชจ๋‹ฌ ๋ณด์ด๊ฒŒ ํ•˜๋Š” ํ•จ์ˆ˜
  const showCartHandler=()=>{
    setCartInShow(true)
  }
  
  //cart๋ชจ๋‹ฌ ์•ˆ๋ณด์ด๊ฒŒ ํ•˜๋Š” ํ•จ์ˆ˜
  const hideCartHandler=()=>{
    setCartInShow(false)
  }

  return (
    // CartProvider ํƒœ๊ทธ ์•ˆ์—์„œ cart-context์˜ ๋‚ด์šฉ์„ ์ „๋ถ€ ๊ฐฑ์‹ /์‚ญ์ œ/์ถ”๊ฐ€ ํ•  ์ˆ˜ ์žˆ์Œ
    <CartProvider>
      { cartInShow && <Cart onClose ={hideCartHandler} />}
      {/* cartInShow๊ฐ€ true์ผ๋•Œ๋งŒ ๋ชจ๋‹ฌ์ด ๋ณด์ž„ */}
      <Header onShowCart={showCartHandler}/>
      <main>
        <Meals />
      </main>
    </CartProvider>
  );
}

export default App;



index.html

<!DOCTYPE html>
<html lang="ko">
  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta
      name="description"
      content="Web site created using create-react-app"
    />

    <title>์ข…ํ•ฉ์˜ˆ์ œ</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <!-- portal ์ง€์ • -->
    <div id="overlay"></div>
    <div id="root"></div>
  </body>
</html>



  • components ํด๋” ๋‚ด Layout ํด๋”
    Header.jsx
import React from 'react'
import classes from "./Header.module.css";
import mealsImg from "../../asset/meals.jpg";
import HeaderCartButton from './HeaderCartButton';
//public์— ๋†“๊ณ  ์ผ๋ฐ˜ ์›นํŽ˜์ด์ง€์—์„œ ์ด๋ฏธ์ง€ ์“ฐ๋“ฏ์ด ์‚ฌ์šฉํ•  ์ˆ˜๋„ ์žˆ์Œ

const Header = (props) => {
  return (
    <>
      <header className={classes.header}>
        <h1>First React Meals</h1>
        {/* onclick : props ์ด๋ฆ„ */}
        <HeaderCartButton onclick={props.onShowCart} />
      </header> 
      <div className={classes["main-image"]}>
        {/* main-image ํด๋ผ์Šค๋Š” ์•ˆ์— -๊ฐ€ ์žˆ์–ด์„œ .์„ ๋ชป ์”€ */}
        <img src={mealsImg} alt="meals" />
      </div>


    </>
  )
}

export default Header;



HeaderCartButton.jsx

import React, { useContext, useEffect, useState } from "react";
import CartIcon from "../Cart/CartIcon";
import classes from "./HeaderCartButton.module.css";
import CartContext from "../../store/cart-context";

// useEffect() : ์–ด๋–ค ์ผ์ด ๋ฐœ์ƒํ•  ๋•Œ๋งˆ๋‹ค ์‹คํ–‰๋˜๋Š” React hook

const HeaderCartButton = (props) => {
  const cartCtx = useContext(CartContext);
  // const numberOfCartItems = cartCtx.items.length; // = ์•„์ดํ…œ ์ˆซ์ž๋ฅผ ์˜๋ฏธ -> ์•„์ดํ…œ ์•ˆ์˜ amount๋ฅผ ํ•ฉํ•ด์ค˜์•ผ ํ•จ
  const [btnIsHigh, setBtnIsHigh] = useState(false); // ๋ฒ„ํŠผ ์ƒํƒœ(์• ๋‹ˆ๋ฉ”์ด์…˜ ์ ์šฉ ์—ฌ๋ถ€)
  const { items } = cartCtx; // ๊ตฌ์กฐ๋ถ„ํ•ด

  // ์ปจํ…์ŠคํŠธ์— ๋ฐฐ์—ด์ด ๋ฐ”๋€” ๋•Œ ์ ์šฉ
  useEffect(() => {
    // ํ•ญ๋ชฉ์ด ์—†์„ ๋•Œ
    if (items.length === 0) {
      return;
    }
    setBtnIsHigh(true);

    const timer = setTimeout(() => {
      setBtnIsHigh(false);
    }, 300);

    // ์‚ฌ์ด๋“œ์ดํŽ™ํŠธ ์ •๋ฆฌ, ํด๋ฆฐ์—… ํ•จ์ˆ˜ (๋ˆˆ์— ๋ณด์ด์ง€ ์•Š์ง€๋งŒ ์ฝ”๋“œ์ ์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ๊น”๋”ํ•˜๊ฒŒ ์ •๋ฆฌํ•จ.)
    return () => {
      clearTimeout(timer);
    };
  }, [items]);

  // ์•„์ดํ…œ์— ๋ณ€ํ™”๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ bump ํด๋ž˜์Šค ๋“ค์–ด๊ฐ
  const btnClass = `${classes.button} ${btnIsHigh ? classes.bump : ""}`;

  // ์œ„์—์„œ ๊ตฌ์กฐ๋ถ„ํ•ด ํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— cartCtx.items๋ผ๊ณ  ์•ˆ ์จ๋„ ๋จ
  const numberOfCartItems = items.reduce((sum, item) => {
    return (sum += item.amount);
  }, 0);
  // ๋ฐฐ์—ด.reduce((ํ•ฉํ•ด์ง„ ๊ฐ’, value) => { ํ•ฉํ•ด์ง„๊ฐ’ + ๋ฐธ๋ฅ˜ }, ํ•ฉํ•ด์ง„ ๊ฐ’์˜ ์ดˆ๊ธฐ๊ฐ’);

  return (
    <button className={btnClass} onClick={props.onclick}>
      <span className={classes.icon}>
        <CartIcon />
      </span>
      <span>Your Cart</span>
      <span className={classes.badge}>{numberOfCartItems}</span>
    </button>
  );
};

export default HeaderCartButton;



  • components ํด๋” ๋‚ด UI ํด๋”
    Card.jsx
import React from 'react'
import classes from "./Card.module.css";

const Card = (props) => {
  return (
    <div className={classes.card}>{props.children}</div>
  )
}

export default Card



input.jsx

import React from "react";
import classes from "./Input.module.css";

const Input = props => {
  return (
    <div className={classes.input}>
      <label htmlFor={props.input.id}>{props.label}</label>
      <input ref={props.propsRef} {...props.input} />
    </div>
  );
};

export default Input;

/* 
  forwardRef()
    - ์ „๋‹ฌ๋ฐ›์€ ref ์–ดํŠธ๋ฆฌ๋ทฐํŠธ๋ฅผ ํ•˜๋ถ€ ํŠธ๋ฆฌ ๋‚ด์˜ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ๋กœ ์ „๋‹ฌํ•  ๋•Œ ํ•จ์ˆ˜ ๋ถ€๋ถ„์„ ๊ฐ์‹ธ์„œ ์‚ฌ์šฉ
*/



Modal.jsx

//๋ชจ๋‹ฌ ํŒ์—…์ฐฝ
import React from 'react'
import ReactDOM from 'react-dom';
import classes from './Modal.module.css'

//๋ชจ๋‹ฌ๋’ค ๊นŒ๋งŒ ๋ฐ˜ํˆฌ๋ช…
const Backdrop=(props)=>{
  return <div className={classes.backdrop} onClick={props.onClose} > </div>
}

//์‹ค์ œ ๋ชจ๋‹ฌ(๊ฐ€์šด๋ฐ ํ•˜์–€๋„ค๋ชจ)
const ModalOverlay=(props)=>{
  return (
    <div className={classes.modal}>
      <div>{props.children}</div>
    </div>
  )
};


// portal์„ ์ถœ๋ ฅํ•  ์œ„์น˜๋ฅผ ๊ฐ€์ ธ์˜ด
const portalElement = document.getElementById('overlay');

//๋ฉ”์ธ ์ปดํฌ๋„ŒํŠธ
const Modal = (props) => {
  return (
    <div>
      {/* {ReactDOM.createPortal(<Backdrop onClose={props.onClose} />,portalElement)}
      {ReactDOM.createPortal(<ModalOverlay>{props.children}</ModalOverlay>, portalElement )}       */}
      {ReactDOM.createPortal(<Backdrop onClose={props.onClose} />, portalElement)}
      {ReactDOM.createPortal(<ModalOverlay>{props.children}</ModalOverlay>, portalElement)}
    </div>
  )
}

export default Modal;
//  createPortal(child(์ž์‹์š”์†Œ), container(ํฌํƒˆ์ด๋ฆ„))



  • components ํด๋” ๋‚ด Meals ํด๋”
    Meals.jsx
import React from 'react'
import AvailableMeals from './AvailableMeals'
import MealsSummary from './MealsSummary'

const Meals = () => {
  return (
    <>
      <MealsSummary />
      <AvailableMeals />
    </>
  )
}

export default Meals



MealsSummary.jsx

import React from "react";
import classes from "./MealsSummary.module.css";

const MealsSummary = () => {
  return (
    <section className={classes.summary}>
      {/* <h2>MealsSummary.js --</h2> */}
      <p>
        Choose your favorite meal from our broad selection of available meals
        and enjoy a delicious lunch or dinner at home.
        </p>
      
      <p>
        All our meals are cooked with high-quality ingredients, just-in-time and of course by experienced
        chefs!
      </p>
    </section>
  );
};

export default MealsSummary;



AvailableMeals.jsx

import React, { useEffect, useState } from "react";
import Card from "../UI/Card";
import classes from "./AvailableMeals.module.css";
import MealItem from "./MealsItem/MealItem";

// const DUMMY_MEALS = [
//   {
//     id: "m1",
//     name: "Sushi",
//     description: "Finest fish and veggies",
//     price: 22.9998,
//   },
//   {
//     id: "m2",
//     name: "Schnitzel",
//     description: "A german specialty!",
//     price: 16.5,
//   },
//   {
//     id: "m3",
//     name: "Barbecue Burger",
//     description: "American, raw, meaty",
//     price: 12.991,
//   },
//   {
//     id: "m4",
//     name: "Green Bowl",
//     description: "Healthy...and green...",
//     price: 18.993,
//   },
// ];

const AvailableMeals = () => {
  const [meals, setMeals] = useState([]); // ๋ฐฑ์—”๋“œ์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋ฉ”๋‰ด
  const [isLoading, setIsLoading] = useState(true); // ํ™”๋ฉด์ด ์—ด๋ฆฌ๋ฉด ๋ฌด์กฐ๊ฑด ๋กœ๋”ฉํ™”๋ฉด์ด ๋œจ๋„๋ก true ์„ค์ •
  const [httpError, setHttpError] = useState(); // ์—๋Ÿฌ ๋ฌธ๊ตฌ๋ฅผ ์ €์žฅํ•˜๊ธฐ ์œ„ํ•œ ์ƒํƒœ ์„ค์ •

  useEffect(()=>{
    // ๋น„๋™๊ธฐ ํ•จ์ˆ˜๋Š” ๋ฆฌํ„ด๊ฐ’์ด promise๋กœ ๋‚˜์˜ด
    const fetchMeals = async () =>{
      const response = await fetch('https://meal-2269f-default-rtdb.asia-southeast1.firebasedatabase.app/meals.json');

      // fetch์˜ ๊ฒฐ๊ณผ๋ฌผ(๋ฆฌํ„ด๊ฐ’) = response.ok = response๊ฐ€ true
      if (!response.ok) {
        throw new Error("์—๋Ÿฌ ๋ฐœ์ƒ๐Ÿ’ฅ");
      }

      const data = await response.json()  //๊ฐ์ฒด ํ˜•์‹์œผ๋กœ ์ €์žฅ๋จ
      console.log('data?',data)

      const loadMeals = [];

      for (const key in data) {
        loadMeals.push({
          id: key,
          name: data[key].name,
          description: data[key].description,
          price: data[key].price
        })
      }
      setMeals(loadMeals);
      setIsLoading(false);
    }

    /*
    try {
      fetchMeals();
    } catch (error) {
      setIsLoading(false);
      setHttpError(error.message);
    }
    */
    // fetchMeals๋Š” async๋ฅผ ์‚ฌ์šฉ -> promise๋ฅผ ๋ฐ˜ํ™˜ -> ์—๋Ÿฌ (useEffect ์•ˆ์—์„œ ์‚ฌ์šฉํ–ˆ๊ธฐ ๋•Œ๋ฌธ์— ๋ฌธ์ œ ๋ฐœ์ƒ)
    // ํ•ด๊ฒฐ๋ฐฉ๋ฒ• - ๋ณ„๋„์˜ ํ•จ์ˆ˜๋ฅผ ํ•œ ๊ฐœ ๋” ์ถ”๊ฐ€ or ์•„๋ž˜ ๋ฐฉ๋ฒ•

      fetchMeals().catch((error) => {
        setIsLoading(false);
        setHttpError(error.message);
      });
    
    // fetchMeals();
  }, [])
  // console.log("meals๋Š”? : ", meals);

  // ๋กœ๋”ฉ์ฒ˜๋ฆฌ
  if(isLoading){
    return(
      <section className={classes.mealsLoading}>
        <p>Loading....</p>
      </section>
    )
  }

  // ์—๋Ÿฌ์ฒ˜๋ฆฌ
  if(httpError) {
    return(
      <section className={classes.mealsError}>
        <p>{httpError}</p>
      </section>
    )
  }

  const mealsList = meals.map((meal)=>
    <MealItem 
      key={meal.id} 
      id={meal.id} 
      name={meal.name} 
      description={meal.description} 
      price={meal.price}/>
  )

  return (
    <section className={classes.meals}>
      <Card>
        <ul>
          {mealsList}
        </ul>
      </Card>
    </section>
  )
}

export default AvailableMeals;



  • Meals ํด๋” ๋‚ด MealsItem ํด๋”
    MealItem.jsx
import React, { useContext } from "react";
import classes from "./MealItem.module.css";
import MealItemForm from "./MealItemForm";
import CartContext from "../../../store/cart-context";

const MealItem = (props) => {
  const cartCtx = useContext(CartContext);
  const price = `$${props.price.toFixed(2)}`; 
  // toFixed(n) : ์†Œ์ˆ˜์  n๋ฒˆ์งธ ์ž๋ฆฌ๊นŒ์ง€๋งŒ ์ถœ๋ ฅ(๋ฐ˜์˜ฌ๋ฆผ). ์ฒซ ๋ฒˆ์งธ ์ž๋ฆฌ๊นŒ์ง€๋งŒ ์žˆ์œผ๋ฉด ๋‘ ๋ฒˆ์งธ ์ž๋ฆฌ๋Š” 0์œผ๋กœ ์ถœ๋ ฅ

  // ํ•จ์ˆ˜ ์ •์˜
  // context์— ์ „๋‹ฌํ•˜๋Š” ํ•จ์ˆ˜ (amount : ์ˆ˜๋Ÿ‰)
  // ์ธ์ž๊ฐ’์€ MealItemForm์—์„œ value๋ฅผ ๋ฐ›์•„์˜ด
  const addToCartHandler = (amount) => {
    console.log("์ˆ˜๋Ÿ‰์€? : ", amount);
    
    // ์—ฌ๊ธฐ์„œ addItem์„ ๊ฐ€์ ธ์™€์„œ ์ž‘๋™์‹œํ‚ด
    cartCtx.addItem({
      id: props.id,
      name: props.name,
      amount: amount, // amount๋กœ ๋ฐ›์•„์™€์„œ
      price: props.price,
    });
  };

  return (
    <li className={classes.meal}>
      <div>
        <h3>{props.name}</h3>
        <div className={classes.description}>{props.description}</div>
        <div className={classes.price}>{price}</div>
      </div>
      <div>
        {/* mealitem์—์„œ mealitemform์œผ๋กœ ๋ณด๋ƒ„ (props) */}
        <MealItemForm id={props.id} onAddToCart={addToCartHandler} />
      </div>
    </li>
  );
};

export default MealItem;



MealItemForm.jsx

import React, { useRef } from "react";
import Input from "../../UI/Input";
import classes from "./MealItemForm.module.css";

const MealItemForm = (props) => {
  // ref๋ฅผ ํ†ตํ•ด์„œ ์ž…๋ ฅ๋œ ๊ฐ’์„ ๋ฐ›์•„์˜ด(ํŠน์ • DOM์„ ์„ ํƒํ•  ๋•Œ ์‚ฌ์šฉ)
  const amountInputRef = useRef();

  const submitHandler = (event) => {
    event.preventDefault();
    const enteredAmount = amountInputRef.current.value; // string
    // const enteredAmountNumber = Number(enteredAmount); // number
    const enteredAmountNumber = +enteredAmount; // number (์ˆซ์ž์—ด๋กœ ๋ณ€๊ฒฝ)

    // console.log(typeof enteredAmountNumber);
    // ์ˆ˜๋Ÿ‰์„ onAddToCart์˜ ์ธ์ž๊ฐ’์œผ๋กœ ๋ฐ›์•„ Mealitem์— ๋„˜๊ฒจ์คŒ
    props.onAddToCart(enteredAmountNumber);
    
    /*
    // ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
    // ๊ณต๋ฐฑ์ด ์žˆ๋Š”๊ฐ€
    if (enteredAmount.trim().length === 0 || enteredAmount < 1 || enteredAmount > 5) {
      return;
    }
    */
  };

  return (
    <form className={classes.form} onSubmit={submitHandler}>
      <Input
        label="Amount"
        propsRef={amountInputRef} // props
        input={{
          id: "amount_" + props.id,
          type: "number",
          min: "1",
          max: "5",
          defaultValue: "1",
          step: "1",
        }}
      />

      {/* <input
        type="number"
        min="1"
        max="5"
        defaultValue="1"
        step="1"
        id={props.id}
      /> */}
      <button>+ Add</button>
    </form>
  );
};

export default MealItemForm;



  • components ํด๋” ๋‚ด Cart ํด๋”
    Cart.jsx
//๋ชจ๋‹ฌํŒ์—… ์•ˆ์— ๋“ค์–ด๊ฐˆ ๋‚ด์šฉ
import React, { useContext, useState } from "react";
import Modal from "../UI/Modal";
import classes from "./Cart.module.css";
import CartContext from "../../store/cart-context";
import Checkout from "./Checkout";

const Cart = (props) => {
  const [isCheckout, setIsCheckout] = useState(false); // ์ฃผ๋ฌธ์‹œ ์ฃผ์†Œ ๋ณด์ด๊ฒŒ/์•ˆ๋ณด์ด๊ฒŒ ํ•˜๋Š” state
  const [isSubmitting, setIsSubmitting] = useState(false); // ์ œ์ถœ์ค‘ ํ‘œ์‹œ
  const [didSubmit, setDidSubmit] = useState(false) // ์ œ์ถœ ์™„๋ฃŒ

  const cartCtx = useContext(CartContext);
  const hasItems = cartCtx.items.length > 0; // ์žฅ๋ฐ”๊ตฌ๋‹ˆ์— ํ•ญ๋ชฉ์ด ์žˆ์„ ๊ฒฝ์šฐ
  const totalAmount = `$ ${cartCtx.totalAmount.toFixed(2)}`;

  const cartItemRemoveHandler = (id) => {
    cartCtx.removeItem(id);
  };
  const cartItemAddHandler = (item) => {
    cartCtx.addItem({ ...item, amount: 1 });
  };


  // orderHandler ํ•จ์ˆ˜ ์ •์˜
  // order ๋ฒ„ํŠผ์„ ํด๋ฆญํ•˜๋ฉด setIsCheckout์ด ๋ฐœ๋™๋˜์–ด input์ฐฝ์ด ๋ณด์ด๋„๋ก ํ•จ
  const orderHandler = () => { setIsCheckout(true); };
  
  // submitOrderHandler ํ•จ์ˆ˜ ์ •์˜
  // ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ firebase์— ๋ณด๋‚ด๋Š” ํ•จ์ˆ˜
  const submitOrderHandler = async (userData) => { // property
    setIsSubmitting(true);
    // Cart.jsx์—์„œ props๋กœ input์— ์ž…๋ ฅํ•œ ๊ฐ’ ๋ฐ›์•„์˜ด
    // console.log("userData : ", userData);
    // console.log("cartCtx.items", cartCtx.items);
    const response = await fetch("https://meal-57688-default-rtdb.asia-southeast1.firebasedatabase.app/orders.json", {
      // restAPI
      method: "POST",
      body: JSON.stringify({
        user: userData,
        orderItems: cartCtx.items
      })
    })
    setIsSubmitting(false);
    setDidSubmit(true);
    cartCtx.clearCart(); // ์ œ์ถœ ์™„๋ฃŒ ํ›„ ์นดํŠธ ๋น„์›Œ์คŒ (cartCtx๋กœ ๋ฐ›์•„์˜ด)
  };

    // ๊ณ„์† ๋ณ€ํ™”ํ•˜๋Š” ๊ฐ’
    const cartItem = (
      <ul className={classes["cart-items"]}>
        {/* li๋ฅผ map์œผ๋กœ ๋Œ๋ฆผ */}
        {cartCtx.items.map((item) => (
          <li key={item.id}>
            <div>
              <h2>{item.name}</h2>
              <div>
                <span className={classes.price}>{`$${item.price.toFixed(
                  2
                )}`}</span>
                <span className={classes.amount}>x {item.amount}</span>
              </div>
            </div>
            <div className={classes.btns}>
              <button onClick={() => cartItemRemoveHandler(item.id)}>-</button>
              <button onClick={() => cartItemAddHandler(item)}>+</button>
            </div>
          </li>
        ))}
      </ul>
    );

  // ๋ฒ„ํŠผ ๋ถ€๋ถ„์„ ๊ฐ€์ ธ์™€์„œ ์ƒ์ˆ˜๋กœ
  const modalActions = (
    <div className={classes.action}>
      <button className={classes["button-outline"]} onClick={props.onClose}>
        Close
      </button>
      {/* hasItems๊ฐ€ true์ผ ๋•Œ๋งŒ Order๋ฒ„ํŠผ์ด ๋ณด์ด๊ฒŒ */}
      {hasItems && (
        <button className={classes.button} onClick={orderHandler}>
          Order
        </button>
      )}
    </div>
  );

  // Modal ์•ˆ์— ๋“ค์–ด๊ฐˆ ๋‚ด์šฉ์„ ์ƒ์ˆ˜๋กœ ๋งŒ๋“ฆ
  const cartModalContent = (
    <div>
      {cartItem}
      <div className={classes.total}>
        <span>Total Amount</span>
        <span>{totalAmount}</span>
      </div>

      {/* Order ๋ฒ„ํŠผ์„ ํด๋ฆญํ–ˆ์„ ๋•Œ๋งŒ ๋‚˜ํƒ€๋‚˜๊ฒŒ */}
      {isCheckout && (
        <Checkout onClick={props.onClose} onConfirm={submitOrderHandler} />
      )}
      {!isCheckout && modalActions}
    </div>
  )

  // ์ œ์ถœํ•˜๋Š” ์ค‘์ผ ๋•Œ ํ‘œ์‹œ๋˜๋Š” ๋‚ด์šฉ
  const isSubmittingModalContent = (<p className={classes.submitting}>์ฃผ๋ฌธ์ค‘์ž…๋‹ˆ๋‹ค.</p>);

  // ์ œ์ถœ ์™„๋ฃŒ ํ›„ ํ‘œ์‹œ๋˜๋Š” ๋‚ด์šฉ
  const didSubmitModalContent = (
    <div>
      <p className={classes.didSubmit}>์ฃผ๋ฌธ์„ ์™„๋ฃŒํ–ˆ์Šต๋‹ˆ๋‹ค.</p>

      <div className={classes.action}>
        <button className={classes["button-outline"]} onClick={props.onClose}>
          Close
        </button>
      </div>
    </div>
  );

  return (
    // ๋ชจ๋‹ฌ ์•ˆ์— ๋ชจ๋“  ๋‚ด์šฉ์„ ์ง‘์–ด๋„ฃ์Œ
    <Modal onClose={props.onClose}>
      {/* ์ œ์ถœ์ค‘ */}
      {isSubmitting && isSubmittingModalContent}
      {/* ์ œ์ถœ ์™„๋ฃŒ */}
      {/* cartModalContent๊ฐ€ ๋ณด์ผ ๋•Œ๋Š” ์™„๋ฃŒ ๋ฒ„ํŠผ์ด ์—†์–ด์•ผ ํ•จ */}
      {!isSubmitting && !didSubmit && cartModalContent}

      {!isSubmitting && didSubmit && didSubmitModalContent}
    </Modal>
  );
};

export default Cart;



CartIcon.jsx

import React from 'react'

const CartIcon = () => {
  return (
    <svg fill="#fff" viewBox="0 0 576 512" width="20px" height="20px" xmlns="http://www.w3.org/2000/svg"><path d="M463.1 416c-26.51 0-47.1 21.49-47.1 48s21.49 48 47.1 48s47.1-21.49 47.1-48S490.5 416 463.1 416zM175.1 416c-26.51 0-47.1 21.49-47.1 48S149.5 512 175.1 512s47.1-21.49 47.1-48S202.5 416 175.1 416zM569.5 44.73c-6.109-8.094-15.42-12.73-25.56-12.73H121.1L119.6 19.51C117.4 8.19 107.5 0 96 0H23.1C10.75 0 0 10.75 0 23.1S10.75 48 23.1 48h52.14l60.28 316.5C138.6 375.8 148.5 384 160 384H488c13.25 0 24-10.75 24-23.1C512 346.7 501.3 336 488 336H179.9L170.7 288h318.4c14.29 0 26.84-9.47 30.77-23.21l54.86-191.1C577.5 63.05 575.6 52.83 569.5 44.73z"/></svg>
  )
}

export default CartIcon




  • store ํด๋”
    cart-context.js
import React from "react";

// ์ปจํ…์ŠคํŠธ๋ฅผ ๋งŒ๋“ฆ, ์ปจํ…์ŠคํŠธ ์•ˆ์— ๋ฐ์ดํƒ€๊ฐ€ ์žˆ์Œ (์ดˆ๊ธฐํ™”)
const CartContext = React.createContext({
  items: [], // ์•„์ดํ…œ์ด ๋“ค์–ด์žˆ๋Š” ๋ฐฐ์—ด
  totalAmount: 0, // ์ด ๊ธˆ์•ก
  addItem: (item) => {},
  removeItem: (id) => {},
  clearCart: () => {}
});
// ์žฅ๋ฐ”๊ตฌ๋‹ˆ ํ•ญ๋ชฉ ์ดˆ๊ธฐ๊ฐ’

export default CartContext;



CartProvider.jsx

import React, { useReducer } from "react";
import CartContext from "./cart-context";
// CartContext = cart-context.js ์•ˆ์˜ 4๊ฐ€์ง€ ๋ฐ์ดํ„ฐ (์ดˆ๊ธฐํ™”)

// reducer ํ•จ์ˆ˜ ์ •์˜
const cartReducer = (state, action) => {
  if (action.type === "ADD") {
    const updatedTotalAmount =
      state.totalAmount + action.item.price * action.item.amount;
    // ๊ธฐ์กด์˜ totalAmount + ๋‚ด๊ฐ€ ๊ฐ€์ ธ์˜จ item์˜ ๊ฐ€๊ฒฉ * ๋‚ด๊ฐ€ ๋ณด๋‚ธ ์•„์ดํ…œ์˜

    // * ๊ฒ€์‚ฌ
    // state.item - ๊ธฐ์กด ๋ฐฐ์—ด / Array.findIndex() - ์ œ์ผ ๋จผ์ € ๋‚˜์˜ค๋Š” ์กฐ๊ฑด์— ๋งž๋Š” ์•„์ดํ…œ์˜ ์ธ๋ฑ์Šค(์ˆœ์„œ) ๋ฐ˜ํ™˜
    // item์˜ id์™€ ์•ก์…˜์œผ๋กœ ์ฐพ์•„์˜จ item์˜ id
    const existingCartItemIndex = state.items.findIndex(
      (item) => item.id === action.item.id
    ); // ์ˆซ์ž๋งŒ ๋ฆฌํ„ด
    const existingCartItem = state.items[existingCartItemIndex]; // item ์ž์ฒด๋ฅผ ๋ฆฌํ„ด
    // ๊ธฐ์กด์—
    // console.log("๊ธฐ์กด์— ๋™์ผํ•œ ์•„์ดํ…œ์ด ์žˆ๋Š”๊ฐ€? : ", existingCartItem);

    let updatedItems;
    // ์ถ”๊ฐ€ํ•œ ์•„์ดํ…œ์ด ๊ธฐ์กด์— ์žˆ๋Š” ์•„์ดํ…œ์ผ ๊ฒฝ์šฐ
    if (existingCartItem) {
      const updatedItem = {
        ...existingCartItem,
        amount: existingCartItem.amount + action.item.amount,
      };
      updatedItems = [...state.items]; // ๊ธฐ์กด์˜ ๊ฐ์ฒด๋ฅผ ์ƒˆ ๋ฐฐ์—ด๋กœ
      updatedItems[existingCartItemIndex] = updatedItem; // ๊ฐ’์„ ๋”ํ•ด์ค€ ๊ธฐ์กด ์•„์ดํ…œ ์—…๋ฐ์ดํŠธ
    } else {
      // ์ถ”๊ฐ€ํ•œ ์•„์ดํ…œ์ด ๊ธฐ์กด์— ์—†๋Š” ์•„์ดํ…œ์ผ ๊ฒฝ์šฐ
      updatedItems = state.items.concat(action.item);
    }

    return {
      items: updatedItems,
      totalAmount: updatedTotalAmount,
    };
  }

  if (action.type === "REMOVE") {
    const existingCartItemIndex = state.items.findIndex(
      (item) => item.id === action.id
    );
    const existingCartItem = state.items[existingCartItemIndex]; //๊ธฐ์กด์•„์ดํ…œ์ด ์—†์„ ๊ฒฝ์šฐ๋Š” undefined

    // console.log("๊ธฐ์กด์— ๋™์ผํ•œ ์•„์ดํ…œ์ด ์žˆ๋Š”๊ฐ€? : ", existingCartItem);

    const updatedTotalAmount = state.totalAmount - existingCartItem.price;

    let updatedItems;
    // ์ถ”๊ฐ€ํ•œ ์•„์ดํ…œ์ด ๊ธฐ์กด์— ์žˆ๋Š” ์•„์ดํ…œ์ผ ๊ฒฝ์šฐ
    if (existingCartItem.amount === 1) {
      // 1์ธ ์ƒํƒœ์—์„œ ๋นผ์ฃผ๋ฉด ์™„์ „ํžˆ ์‚ฌ๋ผ์ ธ์•ผ ํ•จ
      updatedItems = state.items.filter((item) => item.id !== action.id);
    } else {
      const updatedItem = {
        ...existingCartItem,
        amount: existingCartItem.amount - 1,
      };
      updatedItems = [...state.items]; // ๊ธฐ์กด์˜ ๊ฐ์ฒด๋ฅผ ์ƒˆ ๋ฐฐ์—ด๋กœ
      updatedItems[existingCartItemIndex] = updatedItem; // ๊ฐ’์„ ๋”ํ•ด์ค€ ๊ธฐ์กด ์•„์ดํ…œ ์—…๋ฐ์ดํŠธ
    }
    return {
      items: updatedItems,
      totalAmount: updatedTotalAmount,
    };
  }

  // switch (action.type) {
  //   case "ADD":
  //     return {
  //       items: state.items.concat(action.item), // ?
  //       totalAmount: state.items.totalAmount + action.item.price * action.item.amount, // ?
  //     }
  // }

  // *์ฃผ๋ฌธ ์ œ์ถœ ํ›„ cart๋ฅผ ๋น„์›Œ์ฃผ๋Š” action ์ถ”๊ฐ€ (์•ˆ ์จ๋„ ์›๋ž˜ ์žˆ๋˜ return defaultCartState๊ฐ€ ์ž‘๋™๋จ)
  if (action.type === "CLEAR") {
    return defaultCartState
  }

  return defaultCartState;
};

// reducer ์ดˆ๊ธฐํ™” ์ •์˜
const defaultCartState = {
  // ์•„๋ž˜ ๋‘ ๊ฐœ์˜ ์ƒํƒœ ๊ด€๋ฆฌ(addItemToCartHandler๋กœ ์ƒํƒœ๊ด€๋ฆฌ)
  items: [], // ์•„์ดํ…œ์ด ๋“ค์–ด์žˆ๋Š” ๋ฐฐ์—ด
  totalAmount: 0, // ์ด ๊ธˆ์•ก
};

// 2
const CartProvider = (props) => {
  // useReducer ํ˜ธ์ถœ(์„ ์–ธ)
  // dispatchCartAction์€ cartReducer ํ•จ์ˆ˜์— action์œผ๋กœ ๋ณด๋‚ด๋Š” ์—ญํ• ์„ ํ•จ
  const [cartState, dispatchCartAction] = useReducer(cartReducer, defaultCartState);

  // 2-1
  const addItemToCartHandler = (item) => {
    // dispatchCartAction์œผ๋กœ ์š”์†Œ๋“ค์„ ๊ฐ์ฒด ํ˜•์‹์œผ๋กœ ๋ณด๋ƒ„ (ํƒ€์ž…๊ณผ ํ˜„์žฌ ์•„์ดํ…œ)
    // dispatchCartAction์€ cartReducer ํ•จ์ˆ˜์— action์œผ๋กœ ๋ณด๋‚ด๋Š” ์—ญํ• ์„ ํ•จ
    dispatchCartAction({ type: "ADD", item: item });
  };
  const removeItemFromCartHandler = (id) => {
    dispatchCartAction({ type: "REMOVE", id: id });
  };

  // cart๋ฅผ ์ง€์›Œ์ฃผ๋Š” ํ•จ์ˆ˜
  const clearCartHandler = () => {
    dispatchCartAction({type: "CLEAR"});
  }

  // 1
  // ์—…๋ฐ์ดํŠธ ๋  ๊ฐ์ฒด - ๋‹ค์ด๋‚˜๋ฏนํ•˜๊ฒŒ ๋ณ€ํ•˜๋Š” ๋ถ€๋ถ„(๊ฒŒ์† ๋ณ€ํ•˜๋Š” ๋ถ€๋ถ„)
  // items, totalAmount ๋‘ ๊ฐœ๋ฅผ ์ €์žฅํ•˜๋Š” ๊ฒƒ์ด ๋ชฉ์ ์ž„. ๋‚˜๋จธ์ง€๋Š” ํ•จ์ˆ˜ ์ •์˜
  const cartContext = {
    items: cartState.items, // ์•„์ดํ…œ์ด ๋“ค์–ด์žˆ๋Š” ๋ฐฐ์—ด
    totalAmount: cartState.totalAmount, // ์ด ๊ฐฏ์ˆ˜
    addItem: addItemToCartHandler,
    removeItem: removeItemFromCartHandler,
    clearCart: clearCartHandler
  };

  return (
    // 1-1
    <CartContext.Provider value={cartContext}>
      {props.children}
    </CartContext.Provider>
  );
};

export default CartProvider;



Checkout.jsx

import React, { useRef, useState } from "react";
import classes from "./Checkout.module.css";

// ์ž…๋ ฅ์–‘์‹ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ
const isEmpty = (value) => value.trim() === ""; // ๋‚ด์šฉ์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธ
const isElevenChar = (value) => value.trim().length === 11; // 11๊ธ€์ž์ธ์ง€

const Checkout = (props) => {
  // ๋“ค์–ด๊ฐˆ ๋‚ด์šฉ๋“ค ์ •์˜(์˜ค๋ธŒ์ ํŠธํ˜•ํƒœ)
  const [formInputsValidity, setformInputsValidity] = useState({
    name: true,
    address: true,
    tel: true
  });

  const nameInputRef = useRef();
  const addressInputRef = useRef();
  const telInputRef = useRef();
  // console.log("useRef๋Š”? : ", nameInputRef.current);

  // ์ž…๋ ฅํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐฑ์—”๋“œ์— ๋„˜๊ฒจ์ฃผ๋Š” ํ•จ์ˆ˜
  const confirmHandler = (e) => {
    e.preventDefault();

    // ์ž…๋ ฅ๊ฐ’์„ ๋ฐ›์•„์˜ค๋Š” ๋ณ€์ˆ˜ ์ •์˜
    const enteredName = nameInputRef.current.value;
    const enteredAddress = addressInputRef.current.value;
    const enteredTel = telInputRef.current.value;

    // ๊ฐ input์ด ๋น„์–ด์žˆ์ง€ ์•Š์Œ์„ ํ™•์ธ
    const enteredNameValid = !isEmpty(enteredName);
    const enteredAddressValid = !isEmpty(enteredAddress);
    const enteredTelValid = !isEmpty(enteredTel) && isElevenChar(enteredTel) && Number(enteredTel);

    // ํ•˜๋‚˜๋ผ๋„ ๋น„์–ด์žˆ๋Š” input์ด ์žˆ์œผ๋ฉด false
    const formIsValid = enteredNameValid && enteredAddressValid && enteredTelValid; // formIsValid = true

    setformInputsValidity({
      name: enteredNameValid,
      address: enteredAddressValid,
      tel: enteredTelValid
    })
    
    // ๊ฒ€์ฆ
    if (!formIsValid) { return }

    // ๊ฒ€์ฆ ํ›„ ๋ฐ์ดํ„ฐ๋ฅผ Cart.jsx๋กœ ๋„˜๊ฒจ์คŒ
    props.onConfirm({
      name: enteredName,
      address: enteredAddress,
      tel: enteredTel
    })
  }

  const addressControlClass = `${classes.control} ${formInputsValidity.address? "" : classes.invalid}`;
  const telControlClass = `${classes.control} ${formInputsValidity.tel? "" : classes.invalid}`;

  return (
    <form className={classes.form} onSubmit={confirmHandler}>
      {/* order ๋ฒ„ํŠผ์„ ๋ˆ„๋ฅด๋ฉด ๋‚˜ํƒ€๋‚˜๊ฒŒ ํ•จ */}
      <div className={`${classes.control} ${formInputsValidity.name? "" : classes.invalid}`}>
        <label htmlFor="name">์ด๋ฆ„</label>
        <input type="text" id="name" ref={nameInputRef} />
        {/* ์ด๋ฆ„์„ ์•ˆ ์ ์—ˆ์„ ๋•Œ ๋‚˜ํƒ€๋‚˜๊ฒŒ. formInputsValidity๊ฐ€ false์ผ ๋•Œ */}
        {!formInputsValidity.name && <p>์ด๋ฆ„์„ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”</p>}
      </div>

      <div className={addressControlClass}>
        <label htmlFor="address">์ฃผ์†Œ</label>
        <input type="text" id="address" ref={addressInputRef} />
        {!formInputsValidity.name && <p>์ฃผ์†Œ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”</p>}
      </div>

      <div className={telControlClass}>
        <label htmlFor="tel">ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ(์ˆซ์ž๋งŒ 11์ž๋ฆฌ ์ž…๋ ฅํ•˜์„ธ์š”)</label>
        <input type="text" id="tel" ref={telInputRef} />
        {!formInputsValidity.name && <p>ํ•ธ๋“œํฐ ๋ฒˆํ˜ธ๋ฅผ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”(์ˆซ์ž๋งŒ 11์ž๋ฆฌ)</p>}
      </div>

      <div className={classes.action}>
        <button className={classes["button-outline"]} onClick={props.onClick}>Cancel</button>
        {/* ์ž…๋ ฅ ์‹œ ๋ฐฑ์—”๋“œ์— ๋ฐ์ดํ„ฐ ๋„˜๊ฒจ์ฃผ๋„๋ก ํ•˜๋Š” ๋ฒ„ํŠผ. form์— onSubmit์—์„œ ์ž‘๋™ */}
        <button className={classes.button}>Confirm</button>
      </div>
    </form>
  );
};

export default Checkout;

์ถœ๋ ฅ

  • firebase์— ์ €์žฅ๋˜์–ด ์žˆ๋Š” ๋‚ด์šฉ ์ถœ๋ ฅ
  • ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด



  • firebase์— ์ถ”๊ฐ€๋จ







๐Ÿšฉ React Router

React Router

๐Ÿ“ ์„ค๋ช…

  • ์‹ ๊ทœ ํŽ˜์ด์ง€๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ์•Š๊ณ  ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€์—์„œ ๋ Œ๋”๋ง๋˜๊ฒŒ ํ•˜๋Š” ๊ธฐ๋Šฅ

์„ค์น˜ ๋ฐฉ๋ฒ•

npm

  • ์„ค์น˜
    $ npm install react-router-dom localforage match-sorter sort-by
  • ์‹คํ–‰
    $ npm run dev

yarn

  • ์„ค์น˜
    $ yarn add react-router-dom localforage match-sorter sort-by
  • ์‹คํ–‰
    $ yarn start


โœ’๏ธ ์ฝ”๋“œ ์ž‘์„ฑ

์ž…๋ ฅ

App.js

import './App.css';
import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from './pages/Home';
import NotFound from './pages/NotFound';
import Work from './pages/Work';
import Profile from './pages/Profile';

const routers = createBrowserRouter([
  {
    path: "/", // ์ฃผ์†Œ์ฐฝ์— ๋“ค์–ด๊ฐ€๋Š” ํ…์ŠคํŠธ. ("/" - ๊ฐ€์žฅ ์ƒ์œ„ ๋ฌธ์„œ๋ฅผ ๋‚˜ํƒ€๋ƒ„. index.html)
    element: <Home />,
    errorElement: <NotFound />
  },
  {
    path: "/works",
    element: <Work />,
  },
  {
    path: "/profile",
    element: <Profile />,
  },
])

function App() {
  return (
    <>
      <RouterProvider router={routers} />
    </>
  );
}

export default App;



Home.jsx

import React from 'react'

const Home = () => {
  return (
    <div>Home</div>
  )
}

export default Home



Work.jsx

import React from 'react'

const Work = () => {
  return (
    <div>๋‚ด ์ž‘์—…๋ฌผ๋“ค</div>
  )
}

export default Work



NotFound.jsx

import React from 'react'

const NotFound = () => {
  return (
    <div>
      <p>Not FoundโŒ</p>
    </div>
  )
}

export default NotFound



Profile.jsx

import React from 'react'

const Profile = () => {
  return (
    <div>
      <br /><br /><br /><br />
      ํ”„๋กœํ•„์ž…๋‹ˆ๋‹ค.
    </div>
  )
}

export default Profile



์ถœ๋ ฅ

  • ์ด๋ฏธ์ง€๋กœ ๋Œ€์ฒด




๐Ÿ”— ์ฐธ๊ณ  ๋งํฌ & ๋„์›€์ด ๋˜๋Š” ๋งํฌ






profile
๊ณต๋ถ€ํ•˜๋Š” ๋ฒจ๋กœ๊ทธ

0๊ฐœ์˜ ๋Œ“๊ธ€