[React] recoil로 주문내역 관리하기

profile image pIutos 2022. 11. 11. 14:56

학교 수업의 팀 프로젝트인 음식 주문 서비스를 구현하는 과정에서 음식 주문 내역을 추가하거나 수정, 삭제하는 기능을 구현하는 과정을 정리하였다. recoil을 사용하는 이유는 여러 컴포넌트에서 주문 정보를 사용해야하기 때문이다.

recoil의 중첩객체(nested object)를 함수형 업데이트하기

주문 내역을 추가, 삭제하기 이전에 중첩 객체를 함수형 업데이트하는 방법을 알아보자. 보통 useState안의 배열을 수정하기 위해서 아래와 같은 방법으로 함수형 업데이트를 사용해야한다. 

setOrderInfo((prev) => ([...prev, newList]));
import { atom } from "recoil"

export const orderInfoState = atom({
  key: "orderInfo",
  default: {
    finalAmount: 0,
    orderList: [{
      menu: '',
      style: '',
      amount: 0,
      orderListId: 0,
      quantity: 0,
    }]
  }
})

하지만 주문 내역에 필요한 객체는 위와 같이 최종 가격인 finalAmount 속성과 주문 목록 리스트인 orderList 속성이 포함되어있다. 따라서 객체 안의 배열을 함수형 업데이트해야하는데 그 방법은 다음과 같다.

setOrderInfo((prev) => ({
  ...prev, 
  finalAmount: prev.finalAmount + 100,
  orderList: [...prev.orderList, newOrder],
}))

주문 내역 추가하기

위 그림처럼 두번째 선택인 디너 스타일을 선택했을때 주문서에 추가하도록 구현했다.

// order.jsx
const [orderInfo, setOrderInfo] = useRecoilState(orderInfoState);
const menuRef = useRef(null);
const styleRef = useRef(null);
const orderListIdRef = useRef(0);

const onChangeStyleSelect = () => {
  const menu = menuRef.current;
  const style = styleRef.current;
  const totalAmount = totalAmountbyMenuAndStyle(menu.value, style.value);
  
  const newOrder = {
        menu: menu.value, 
        style: style.value, 
        amount: totalAmount, 
        orderListId: orderListIdRef.current, 
        quantity: 1
      };
  orderListIdRef.current += 1;
  setOrderInfo((prev) => ({
    ...prev,
    finalAmount: prev.finalAmount + totalAmount,
    orderList: [...prev.orderList, newOrder]
  }));
  
  menu.value = '';
  style.value = '';
};

return (
  <div className="my-3 flex justify-between">
    <h3 className="text-lg font-bold">디너 메뉴</h3>
    <select ref={menuRef} className="outline-none">
      <option value="">--디너 메뉴를 선택하세요--</option>
      <option value="valentine">Valentine dinner ($100)</option>
      <option value="french">French dinner ($130)</option>
      <option value="english">English dinner ($130)</option>
      <option value="champagne">Champagne Feast dinner ($250)</option>
    </select>
  </div>
  <div className="my-3 flex justify-between">
    <h3 className="text-lg font-bold">디너 스타일</h3>
    <select ref={styleRef} onChange={onChangeStyleSelect} className="outline-none" disabled>
      <option value="">--디너 스타일을 선택하세요--</option>
      <option value="simple">Simple</option>
      <option value="grand">Grand (+ $20)</option>
      <option value="deluxe">Deluxe (+ $40)</option>
    </select>
  </div>
  <h2 className="font-bold text-lg">주문서</h2>
  {orderInfo.orderList
    .filter((list) => list.orderListId > 0)
    .map(order => (
      <OrderSheetBox key={order.orderListId} order={order} orderInfo={orderInfo} setOrderInfo={setOrderInfo} progress={progress}/>
    ))}
  <div className="text-lg font-bold">최종 결제 금액: ${orderInfo.finalAmount}</div>
);

두번째 스타일이 변경되었을 때 메뉴와 수량, 가격, 수량, id를 포함한 새로운 객체를 생성하여 orderList배열에 추가해주었다.

id는 삭제할때와 map에 고유한 key를 주기위해서 생성했는데, useRef를 이용하여 새 주문이 생성될때마다 1을 더한값이 id가 되도록 구현했다.

그리고 totalAmount값은 주문의 메뉴의 가격의 합으로 설정해서 전체 가격에다가 더해주었다. totalAmountbtMenuAndStyle 함수(더보기)

더보기
function totalAmountbyMenuAndStyle(menu, style) {
  let totalAmount = 0;
  switch(menu) {
    case 'valentine': totalAmount += 100; break;
    case 'french': totalAmount += 130; break;
    case 'english': totalAmount += 130; break;
    case 'champagne': totalAmount += 250; break;
    default: totalAmount += 0; 
  }
  switch(style) {
    case 'simple': totalAmount += 0; break;
    case 'grand': totalAmount += 20; break;
    case 'deluxe': totalAmount += 40; break;
  }
  return totalAmount;
}

TroubleShooting: 동일 주문은 수량만 증가하도록 처리하기

위의 동작처럼 동일한 주문인데도 주문서에서 각각의 다른 주문으로 추가된다. 따라서 두 주문을 한번에 수량과 가격만 증가하도록 하는 처리가 필요하다.

const orderListIdRef = useRef(0);
const onChangeStyleSelect = () => {
  const menu = menuRef.current;
  const style = styleRef.current;
  const totalAmount = totalAmountbyMenuAndStyle(menu.value, style.value);

  const sameOrder = orderInfo.orderList.filter(order => order.menu === menu.value && order.style === style.value)
  if(sameOrder.length === 0) {
    orderListIdRef.current += 1;
    setOrderInfo((prev) => ({
      ...prev,
      finalAmount: prev.finalAmount + totalAmount,
      orderList: [...prev.orderList, {menu: menu.value, style: style.value, amount: totalAmount, orderListId: orderListIdRef.current, quantity: 1}]
    }));
  } else {
    const newSameOrder = {...sameOrder[0], quantity: parseInt(sameOrder[0].quantity) + 1, amount: sameOrder[0].amount + totalAmount}
    const orderListExcept = orderInfo.orderList.filter(order => order.orderListId !== sameOrder[0].orderListId);
    
    setOrderInfo((prev) => ({
      ...prev, 
      finalAmount: prev.finalAmount + totalAmount,
      orderList: [...orderListExcept, newSameOrder]
    }))
  }
  menu.value = '';
  style.value = '';
};

orderList에 필터 함수를 적용해서 menu와 style이 같은 객체의 배열을 가져올 수 있다. 이 배열의 길이가 0이라면 기존 작업을 수행하면 되고, 아니라면 수량과 가격을 증가시킨 newSameOrder 객체와 orderListExecpt 배열을 업데이트한다.

orderListExecpt 배열은 업데이트할 객체 외의 모든 객체를 orderList에서 필터해서 받아온 배열이다.

참고로 sameOrder[0]으로 작성한 이유는 sameOrder는 배열을 반환하므로 [{menu: '', style: '', ...}]와 같기 때문에 배열의 0번 인덱스를 사용해야하기 때문이다!

같은 주문일 때는 수량만 증가하도록 구현된 것을 확인할 수 있다.

주문 수량 수정하기

function OrderBox({order, orderInfo, setOrderInfo, progress}) {
  const onChangeOrderQuantity = (e, orderListId) => {
    const num = e.target.value;
    const orderList = orderInfo.orderList.filter(order => order.orderListId === orderListId)[0];
    const initAmount = orderList.amount / orderList.quantity;
    const newOrderList = {...orderList, quantity: num, amount: initAmount * num}
    const orderListExcept = orderInfo.orderList.filter(order => order.orderListId !== orderListId);
    
    setOrderInfo((prev) => ({
      ...prev, 
      finalAmount: prev.finalAmount - orderList.amount + initAmount * num,
      orderList: [...orderListExcept, newOrderList]
    }))
  }

  return (
    <div className="flex my-1">
      <span>메뉴: {order.menu}</span>
      <span className="ml-2">스타일: {order.style}</span>
      <span className="ml-2">가격: {order.amount}</span>
      <span className="ml-2">주문 수량: </span>
      <input type="number" min="1" value={order.quantity} onChange={e => onChangeOrderQuantity(e, order.orderListId)} className="text-center w-10 outline-none" />
    </div>
  )
}

각 주문을 OrderBox 컴포넌트로 분리하여 작성했다. 여기서 주문 수량이 변경되었을 때 orderInfo객체가 수정되어야 한다.

주문 수량은 증가하거나 감소해야하기 때문에 단순히 수량을 +1로 처리하면 안된다. 따라서 현재 입력된 숫자값을 수량으로 지정한 다음, 수량에따른 주문 가격과 총 가격도 변경시켰다.

총 가격을 구하기 위해서 복잡하게 더하기와 빼기로 구현했는데 이것보다 좋은 방법이 있을 것 같아서 더 찾아봐야겠다..

주문 수량을 변경할 수 있도록 구현된 것을 확인할 수 있다.

TroubleShooting2: 주문 내역 오름차순 정렬하기

위와 같이 주문 수량을 변경할 때마다 배열의 순서가 바뀌어서 주문서에서 표시되는 순서도 같이 변경되는 문제가 발생했다.

이를 해결하기 위해 orderList 배열을 불러올 때 sort함수를 이용해서 오름차순으로 배열을 정렬하도록 구현했다.

{ orderInfo.orderList
  .filter((list) => list.orderListId > 0)
  .sort((a, b) => a.orderListId - b.orderListId)
  .map(order => (
    <OrderBox key={order.orderListId} order={order} orderInfo={orderInfo} setOrderInfo={setOrderInfo} progress={progress}/>
))}

주문 내역 삭제하기

function OrderBox({order, orderInfo, setOrderInfo, progress}) {
  ...

  const onClickDeleteBtn = (orderListId) => {
    const orderList = orderInfo.orderList.filter(order => order.orderListId === orderListId)[0];
    const orderListExcept = orderInfo.orderList.filter(order => order.orderListId !== orderListId);
    setOrderInfo((prev) => ({
      ...prev, 
      finalAmount: prev.finalAmount - orderList.amount,
      orderList: [...orderListExcept]
    }))
  }

  return (
    <div className="flex my-1">
      <span>메뉴: {order.menu}</span>
      <span className="ml-2">스타일: {order.style}</span>
      <span className="ml-2">가격: {order.amount}</span>
      <span className="ml-2">주문 수량: </span>
      <input type="number" min="1" value={order.quantity} onChange={e => onChangeOrderQuantity(e, order.orderListId)} className="text-center w-10 outline-none" />
      <button onClick={() => onClickDeleteBtn(order.orderListId)} className="px-2 font-bold">X</button>
    </div>
  )
}

삭제 기능을 구현하기위해 삭제 버튼을 만들고, 삭제버튼이 눌린 주문의 id를 삭제 버튼에 전달해서 orderList에 이와 일치하지않는 id의 배열을 추가하였다.(orderListExecpt)

주문을 삭제할 수 있도록 구현된 것을 확인할 수 있다.

+) select 순차 선택하도록 구현하기

주문을 위해서 디너 메뉴 -> 디너 스타일 순으로 선택하기 위해서 메뉴가 선택되기 전에는 스타일 선택은 비활성화(disabled)되어야 한다.

// 순차선택 로직
const styleRef = useRef(null);

const [isSelectedMenu, setIsSelectedMenu] = useState(false);
const onChangeMenuSelect = () => {
  setIsSelectedMenu(true);
}

const onChangeStyleSelect = () => {
  ...
  setIsSelectedMenu(false);
  menu.value = '';
  style.value = '';
};

useEffect(() => {
    const style = styleRef.current;
    if(isSelectedMenu) style.disabled = false;
    else style.disabled = true;
  }, [isSelectedMenu]);

return (
  <div className="my-3 flex justify-between">
    <h3 className="text-lg font-bold">디너 메뉴</h3>
    <select ref={menuRef} name="menu" onChange={onChangeMenuSelect} className="outline-none">
      <option value="">--디너 메뉴를 선택하세요--</option>
      ...
);

순차 선택을 구현하기 위해 useState와 useEffect를 이용해서 앞의 메뉴가 선택되었을 때 스타일 선택이 활성화되도록 구현했다.