음식 주문 앱에 Http 및 양식 추가

맛없는콩두유·2022년 8월 31일
0
post-thumbnail

'식사'데이터를 백엔드로 이동하기

  • AvailableMeals.js

DUMMY로 있던 데이터들을 Firebase로 옮겨서 백엔드 작업을 해보겠습니다!

Http를 통해 식사 가져오기

데이터를 저장시켰고, 이 데이터를 AvailableMeals에서 useEffect와 useState를 이용하여 불러들이는 작업을 하겠습니다.

import { useEffect, useState} from "react";
const AvailableMeals = () => {
  const [meals, setMeals] = useState([]);
  useEffect(() => {
    const fetchMeals = async () => {
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/meals.json"
      );
      const responseData = await response.json();
      const loadedMeals = [];

      for (const key in responseData) {
        loadedMeals.push({
          id: key,
          name: responseData[key].name,
          description: responseData[key].description,
          price: responseData[key].price,
        });
      }

      setMeals(loadedMeals);
    };
    fetchMeals();
  }, []);

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

fetch를 이용하여 Firebase에 있는 주소를 입려하여 불러올 수 있습니다.
주의할 점은 useEffect는 async await인 promise 함수를 반환해서는 안됩니다.
그래서 useEffect에 직접적으로 async를 사용할 수 없고, 새로운 메서드인 fetchMeals를 생성하여 async를 사용합니다. 그리고 현재 responseData는 Firebase에서 객체이기 때문에 for - on을 이용해 객체를 하나하나 key인 id값으로 접근하여 속성을 정의하는 것을 볼 수 있습니다.

로딩 State 다루기

현재 Firebase에서 데이터를 가져오는 것 까지는 완료했지만, 로딩 중인 화면을 띄우지 않았습니다. 로딩을 한번 다뤄보겠습니다.

const [isLoading, setIsLoading] = useState(true);

새로운 State를 추가해주고 기본값은 true로 해줍니다.

  useEffect(() => {
    const fetchMeals = async () => {
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/meals.json"
      );
      const responseData = await response.json();
      const loadedMeals = [];

      for (const key in responseData) {
        loadedMeals.push({
          id: key,
          name: responseData[key].name,
          description: responseData[key].description,
          price: responseData[key].price,
        });
      }

      setMeals(loadedMeals);
      setIsLoading(false);
    };
    fetchMeals();
  }, []);

그런 다음 setMeals로 Meals를 가져오고난 후 Loading을 끝내기 위해 다시 false로 바꿔줍니다.

 if (isLoading) {
    return (
      <section className={classes.MealsLoading}>
        <p>Loading..</p>
      </section>
    );
  }

조건 문으로 로딩일 떄만 Loading... 글씨가 표시되게 적어놓았습니다.


새로고침을 하면 아주 잠깐이지만 Loading...의 글씨가 나오고 데이터가 보여지는 것을 볼 수 있습니다.

오류 처리하기

useEffect(() => {
    const fetchMeals = async () => {
      const response = await fetch(
        "https://react-http-ecb71-default-rtdb.firebaseio.com/meals"
      );

fetch를 전송할 때 url에 주소를 다르게 입력해서 오류를 발생시켜 보겠습니다.

그러면 Loading...으로만 계속 뜨고 데이터가 나오지않는 것을 볼 수 있습니다.

  const [httpError, setHttpError] = useState();

에러를 다루기 위해서 errorState를 추가홰줍니다.

if (!response.ok) {
        throw new Error("Something went wrong!");
      }

response가 false일 떄 에러가 보이게 할 문구를 정해줍니다.


    fetchMeals().catch((error) => {
      setIsLoading(false);
      setHttpError(error.message);
    });

보통 try-catch 를 이용하여 error 메세지를 띄우지만 fetchMeals는 Promise함수이기 때문에 try-catch를 해서 에러를 잡을 수 없습니다. 대신 .catch()를 이용하여 error를 표시하는 것이 포인트입니다.

if (httpError) {
    return (
      <section className={classes.MealsError}>
        <p>{httpError}</p>
      </section>
    );
  }

조건 문으로 에러가 있으면 메시지가 보이게 설정을 하면 완성입니다!


빨강색 글씨로 에러를 확인할 수 있습니다.

결제 양식 추가하기


현재 Cart를 누르면 Modal창으로 장바구니한 목록이 뜨지만 사용자의 정보를 입력하는 정보가 없어 추가해보도록 하겠습니다!

  • Checkout.js
import classes from "./Checkout.module.css";

const Checkout = (props) => {
  const confirmHandler = (event) => {
    event.preventDefault();
  };

  return (
    <form className={classes.form} onSubmit={confirmHandler}>
      <div className={classes.control}>
        <label htmlFor="name">Your Name</label>
        <input type="text" id="name" />
      </div>
      <div className={classes.control}>
        <label htmlFor="street">Street</label>
        <input type="text" id="street" />
      </div>
      <div className={classes.control}>
        <label htmlFor="postal">Postal Code</label>
        <input type="text" id="postal" />
      </div>
      <div className={classes.control}>
        <label htmlFor="city">City</label>
        <input type="text" id="city" />
      </div>
      <div className={classes.actions}>
        <button type="button" onClick={props.onCancel}>
          Cancel
        </button>
        <button className={classes.submit}>Confirm</button>
      </div>
    </form>
  );
};

export default Checkout;

먼저 화면에 보일 Checkout.js를 만들어주고

  • Cart.js
import Checkout from "./Checkout";
<Checkout onCancel={props.onClose} />} 

렌더링을 해줍니다.

그리고 Order를 클릭할 때만 Checkout.js가 보이게 할 예정이니 isCheckoutSate가 필요하다.

const [isCheckout, setIsCheckout] = useState(false);

const orderHandler = () => {
    setIsCheckout(true);
  };

<button className={classes.button} onClick={orderHandler}>
          Order
        </button>

이제 조건부로 Checkout 컴포넌트를 렌더링 가능하다

{isCheckout && <Checkout onCancel={props.onClose} />

당므으로 Order 버튼을 클릭했을 때 양식이 보이고 close버튼과 order버튼을 숨겨보겠습니다. 그리고 양식에 버튼을 추가하여 취소버튼을 만들어 보겠습니다.

  • Cart.js
 const modalActions = (
    <div className={classes.actions}>
      <button className={classes["button--alt"]} onClick={props.onClose}>
        Close
      </button>
      {hasItems && (
        <button className={classes.button} onClick={orderHandler}>
          Order
        </button>
      )}
    </div>
  );

      {!isCheckout && modalActions}
  • Checkout.js
 <button type="button" onClick={props.onCancel}>
          Cancel
        </button>

type을 button으로 설정하여 제출이 안되게 막는다.

  • Cart.js
{isCheckout && <Checkout onCancel={props.onClose} />}

props를 이용하여 전달했습니다.

하려는 의도가 잘 보여지는 것을 알 수 있습니다.

양식 값 읽기

이제 사용자가 입력한 input에 유효성 검사와 오류메시지를 표시하여 서버에 제출하는 것을 해보겠습니다.

키를 누를때 마다 입력된 데이터를 얻고 저장하여 얻는 방법과 전체 form이 제출하면 그것을 얻는 방법이 있습니다.

저는 후자의 전체 form을 이용해 얻는 방법을 선택하기 위해 useRef를 이용해보겠습니다.

  • Checkout.js
  const nameInputRef = useRef();
  const streetInputRef = useRef();
  const postalCodeInputRef = useRef();
  const cityInputRef = useRef();

useRef를 import하고

<input type="text" id="name" ref={nameInputRef} />
<input type="text" id="street" ref={streetInputRef} />
...

ref로 각 input에 연결시켜줌으로써 사용자가 입력한 모든 것을 얻을 수 있습니다.

const confirmHandler = (event) => {
    event.preventDefault();

    const enteredName = nameInputRef.current.value;
    const enteredStreet = streetInputRef.current.value;
    const enteredPostalCode = postalCodeInputRef.current.value;
    const enteredCity = cityInputRef.current.value;

그리고 confirmHandler 메서드에서 current.value 실제 값을 갖고있는 파라미터를 얻어와 변수에 저장합니다.

양식 유효성 검사 추가하기

각 입력에 대해 유효성 검사를 추가해보겠습니다!

  • Checkout.js

const isEmpty = (value) => value.trim() === "";
const isFiveChars = (value) => value.trim().length === 5;

유편번호는 5자로 제한했습니다.

const enteredNameIsValid = !isEmpty(enteredName);
    const enteredStreetIsValid = !isEmpty(enteredStreet);
    const enteredCityIsValid = !isEmpty(enteredCity);
    const enteredPostalCodeIsValid = isFiveChars(enteredPostalCode)

유효한 값들을 각각 저장하고

const formIsValid =
      enteredNameIsValid &&
      enteredStreetIsValid &&
      enteredCityIsValid &&
      enteredPostalCodeIsValid;

각각 전부다 유효한 상태여야만 formIsValid에 저장합니다.

if (!formIsValid) {
      return;
    }

유효하지 않다면 코드실행을 멈추기 위해 return을 하고

그 전에, 몇가지 상태를 업데이트하여 사용자에게 메시지를 전달해보도록 useState를 import하겠습니다.
결합된 상태 슬라이드를 써서 state 해보겠습니다.

const [formInputsValidity, setFormInputsValidity] = useState({
    name: true,
    street: true,
    city: true,
    postalCode: true,
  });

해당 필드가 유효한지 아닌지 여부를 결정할 겁니다 처음에는 이것을 유효하다고
true로 하지만 엄밀히 말하면 그렇지 않다. 오류 메시지를 처음부터 보여주고싶지 않음을 의미한다.

이제 다양한 입력에 대해 올바른 필드를 업데이트하기 위해 양식을 제출하여 true값을 실제 유효성으로 업데이트할 떄 입니다.

그러기 위해서는 새 객체로 FormInputsValidity()를 설정해야 한다.

 setFormInputsValidity({
      name: enteredNameIsValid,
      street: enteredStreetIsValid,
      city: enteredCityIsValid,
      postalCode: enteredPostalCodeIsValid,
    });

return에 각 input문 밑에

{!formInputsValidity.name && <p>Please enter a valid name!</p>}
{!formInputsValidity.street && <p>Please enter a valid street!</p>}
...

를 추가하여 유효하지 않을 때 문구를 입력합니다.


 const nameControlClasses = `${classes.control} ${
    formInputsValidity.name ? "" : classes.invalid
  }`;
  const streetControlClasses = `${classes.control} ${
    formInputsValidity.street ? "" : classes.invalid
  }`;
  const postalCodeControlClasses = `${classes.control} ${
    formInputsValidity.postalCode ? "" : classes.invalid
  }`;
  const cityControlClasses = `${classes.control} ${
    formInputsValidity.city ? "" : classes.invalid
  }`;

<div className={nameControlClasses}>
        <label htmlFor="name">Your Name</label>
        <input type="text" id="name" ref={nameInputRef} />
        {!formInputsValidity.name && <p>Please enter a valid name!</p>}
</div>
<div className={streetControlClasses}>
        <label htmlFor="street">Street</label>
        <input type="text" id="street" ref={streetInputRef} />
        {!formInputsValidity.street && <p>Please enter a valid street!</p>}
</div>
...

그리고 기본 css는 {classes.control}로 주고 상황에 따라 true일때와 false일 때 다른 css를 입력합니다.

유효성 검사가 완료된 것을 볼 수 있습니다.

장바구니 데이터 제출 및 전송하기

입력한 데이터가 Firebase에 전달하도록 만들어보자!
서버가 데이터로 전달되어야 하는 컴포넌트는 Cart 컴포넌트이다.

새 함수를 만들어 보자
Cart.js에 const submitOrderHandler 함수를 추가하여 Checkout 컴포넌트로 호출되고
그것이 해당 userDate를 받았는지 확인해야 한다.

  • Cart.js
const submitOrderHandler = (userData) => {
    fetch("https://react-http-ecb71-default-rtdb.firebaseio.com/orders.json", {
      method: "POST",
      body: JSON.stringify({
        user: userData,
        orderedItems: cartCtx.items,
      }),
    });
  };

그러기 위해서는 Checkout 컴포넌트에 prop를 통해 onConfirm prop을 이용한다.

<Checkout onConfirm={submitOrderHandler} onCancel={props.onClose} />

Cart.js에서 userData를 매개변수로 보내고 있기 때문에

Checkout.js에서 userData를 {}객체 그룹화해보겠다.

  • Checkout.js
   props.onConfirm({
      name: enteredName,
      street: enteredStreet,
      city: enteredCity,
      postalCode: enteredPostalCode,
    });
  };

를 추가한다.


입력한 값이 Firebase에 잘 전달되는 것을 알 수 있다.

더 나은 사용자 피드백 추가하기

이제 submitOrderHandler 상태를 다뤄보겠습니다.

 const [isSubmitting, setIsSubmitting] = useState(false);

submitOrderHandler에 제출이되면
setIsSubmitting(true);를 넣어줍니다.
그리고 fetch 함수가 끝나기를 기다려야 하기위해 await와 async를 붙입니다.
그리고
제출이 끝난 후 setIsSubmitting(false); 입력합니다.

const submitOrderHandler = async (userData) => {
    setIsSubmitting(true);
    await fetch("https://react-http-6b4a6.firebaseio.com/orders.json", {
      method: "POST",
      body: JSON.stringify({
        user: userData,
        orderedItems: cartCtx.items,
      }),
    });
    setIsSubmitting(false);
    setDidSubmit(true);
    cartCtx.clearCart();
  };

그리고 성공 메시지를 표시하기 위해 state를 또 만들겠습니다.

const [didSubmit, setDidSubmit] = useState(false);
const cartModalContent = (
    <React.Fragment>
      {cartItems}
      <div className={classes.total}>
        <span>Total Amount</span>
        <span>{totalAmount}</span>
      </div>
      {isCheckout && (
        <Checkout onConfirm={submitOrderHandler} onCancel={props.onClose} />
      )}
      {!isCheckout && modalActions}
    </React.Fragment>
  );

장바구니 창

const isSubmittingModalContent = <p>Sending order data...</p>;

로딩중일 때 창

 const didSubmitModalContent = (
    <React.Fragment>
      <p>Successfully sent the order!</p>
      <div className={classes.actions}>
        <button className={classes.button} onClick={props.onClose}>
          Close
        </button>
      </div>
    </React.Fragment>
  );

제출버튼이 성공적으로 눌려졌을 떄 제출완료 창

return (
    <Modal onClose={props.onClose}>
      {!isSubmitting && !didSubmit && cartModalContent}
      {isSubmitting && isSubmittingModalContent}
      {!isSubmitting && didSubmit && didSubmitModalContent}
    </Modal>
  );

{!isSubmitting && !didSubmit && cartModalContent}
제출 중이지않고 제출이되지않았을 때는 그냥 Cart 창

{isSubmitting && isSubmittingModalContent}
제출 중일 때는 로딩 창

{!isSubmitting && didSubmit && didSubmitModalContent}
제출 중이지않고 제출이되었을 때는 제출완료 창

마지막으로 제출이 완료되면 Cart를 비워보겠습니다.
cartProvider.js로 이동해야 하는데

  • cartProvider.js

    const cartReducer = (state, action) => {
    	...
     if (action.type === "CLEAR") {
        return defaultCartState;
      }
    }

    이제 CLEAR 액션이 전송될 수 있는지 확인해야 합니다.
    그러려면 새 함수를 추가해야하는데

    const CartProvider = (props) => {
    ...
    
    	const clearCartHandler = () => {
       		dispatchCartAction({ type: "CLEAR" });
     	};
    } 

    정황상 새 함수가 필요해보이는데 cart-context.js에서 더 나은 자동완성 기능을 얻어야하기 떄문에

  • cart-context.js

    const CartContext = React.createContext({
     items: [],
     totalAmount: 0,
     addItem: (item) => {},
     removeItem: (id) => {},
     clearCart: () => {}
    });

    clearCart function을 추가해줍니다.

  • cartProvider.js

    const cartContext = {
    	...
       clearCart: clearCartHandler,
     };

    cartContext에 clearCart: clearCartHandler,를 추가해주면 됩니다.

    그리고 Cart컴포넌트에서 이것을 요청할 수 있습니다

  • Cart.js

    const submitOrderHandler = async (userData) => {
    	...
        cartCtx.clearCart();
    }

    submitOrderHandler 객체 마지막에 제출 후 카트를 비우라는 cartCtx.clearCart(); 메서드를 추가해주면 완성입니다


    양식을 작성 후 제출을 누르면

    성공적으로 메시지가 보이는 것이 확인됩니다!
profile
하루하루 기록하기!

0개의 댓글