패스트캠퍼스X야놀자 프론트엔드 개발 부트캠프 미니 프로젝트

지구·2023년 12월 20일
0

부트캠프

목록 보기
6/6
post-thumbnail

미니 프로젝트

숙박 예약 서비스 구현

패스트캠퍼스X야놀자 프론트엔드 개발 부트캠프에서 이번엔 백엔드 부트캠프 교육생과 함께 숙박 예약 서비스를 구현하는 프로젝트를 진행했다.

미니 프로젝트 일정

11/20(월) ~ 12/01(금), 2주간

프로젝트 정의서

  1. 본 프로젝트는 숙박 예약 서비스를 완성하는 것을 목표로 합니다.
  2. 필요한 설계는 팀별로 직접 구성합니다.
  • 회원 인증
    • 회원가입
    • 로그인
  • 상품 조회
    • 전체 숙박 상품 목록 조회
      (옵션) 카테고리를 임의 생성하여 분류하여 출력
    • 개별 숙박 상품 상세 소개
  • 상품 선택 및 장바구니 담기
    • 숙박 상품 옵션 선택
    • 장바구니 담기
    • (또는) 바로 결제하기
  • 장바구니
    • 장바구니 보기
    • 장바구니에서 주문하기 버튼 클릭 시, 예약(주문) 페이지로 이동
  • 예약(주문) 하기
    • 만 14세 이상 이용 동의 (상세 설명서 X, 체크박스로만 간단히 처리)
    • 결제하기 버튼 클릭 시, 상품을 주문한 것으로 처리
      (별도 결제 로직 없음)
    • 결제 성공 시 주문 결과 출력
  • (옵션) 주문 내역 조회
    • 별도 주문 내역 페이지를 통해 주문 내역 확인

프로젝트 요구사항

  1. 회원 회원가입 기능
    • 회원은 회원가입을 할 수 있습니다.
    • 기본 정보는 ID 역할로 이메일 주소와, 비밀번호, 이름 입니다.
  2. 회원 로그인 기능
    • 이메일과 비밀번호로 로그인할 수 있습니다.
    • 회원 정보를 저장해둔 데이터베이스를 검색하여 해당 사용자가 유효한 사용자 인지 판단합니다.
    • 상품 조회(전체, 개별), 회원 가입은 로그인 없이 사용 가능합니다.
    • 이 외 기능은 로그인이 필요합니다.
  3. 전체 상품 목록 조회
    • 데이터베이스에서 전체 상품 목록을 가져옵니다.
    • 이미지, 상품명, 상품가격을 기본으로 출력합니다.
    • 재고에 따라 품절일 경우, 출력 여부에 대해선 팀별로 결정합니다.
    • (옵션) 카테고리를 분류하여, 상품을 출력할 수도 있습니다.
    • 한 페이지에 출력되는 상품 개수는 팀별로 정하여, 페이징을 수행합니다.
  4. 개별 상품 조회
    • 전체 상품 목록에서 특정 상품 이미지를 클릭하면, 해당 상품에 대한 상세 정보를 상품에 저장해 둔 데이터베이스에서 가져옵니다.
    • 이미지, 상품명, 상품가격, 상품 상세 소개 (1줄 이상)을 기본으로 출력합니다.
    • 재고에 따라 품절일 경우, 화면 구성은 팀별로 결정합니다.
  5. 상품 옵션 선택
    • 상품 상세 소개 페이지에서 상품 옵션을 선택할 수 있습니다.
    • 날짜, 숙박 인원은 기본으로 포함됩니다.
    • 이 외 룸 형태 등 필요한 요소는 팀별로 기획합니다.
  6. 장바구니 담기
    • 상품 옵션을 선택한 후, 장바구니 담기 버튼을 클릭하면 선택한 상품이 장바구니에 담깁니다.
  7. 장바구니 보기
    • 장바구니에 담긴 상품 데이터 (이미지, 상품명, 옵션 등)에 따른 상품별 구매 금액, 전체 주문 합계 금액 등을 화면에 출력합니다.
    • 체크 박스를 통해 결제할 상품을 선택/제외할 수도 있습니다.
    • 주문하기 버튼을 통해 주문/결제 화면으로 이동합니다.
  8. 주문하기
    • 장바구니에서 주문하기 버튼 또는 개별 상품 조회 페이지에서 주문하기 버튼을 누르면 전환되는 페이지입니다.
    • 만 14세 이상 이용 동의를 체크 박스로 입력 받으면, 화면 최하단에 결제하기 버튼이 활성화됩니다.
  9. 결제하기
    • 주문 페이지에서 결제하기 버튼을 클릭하면, 실제 결제 로직 및 절차 없이 상품을 바로 주문한 것으로 처리합니다.
    • 주문을 저장하는 데이터베이스에 주문 정보를 저장합니다.
  10. 주문 결과 확인
    • 결제를 성공적으로 처리하면, 주문한 상품(들)에 대한 주문 결과를 출력해줍니다.
  11. (옵션) 주문 내역 확인
    • 별도 주문 내역 페이지에 여태 주문한 모든 이력을 출력해줍니다.

기능적 요구 사항

  1. 공통
    • 모든 단계에서 협업을 기반으로 프로젝트를 진행합니다.
    • 각 기능을 구현하기 위해 HTTP Request Body / Response Body 에 전달할 데이터는 프론트엔드와 백엔드의 협업을 통해 결정합니다.
    • 모든 단계에서 테스트를 수행합니다.
  2. 백엔드
    • REST API를 구현하여 프론트엔드로 JSON 형식의 데이터를 전달합니다.
    • 회원 인증과 인가는 Spring Security를 이용하여 진행합니다.
    • 숙박 상품에 대한 데이터는 오픈 API를 검증하여 활용합니다.
      선택1. https://www.data.go.kr/data/15077518/openapi.do
      선택2. https://api.visitkorea.or.kr/
    • 전체 상품 조회 시 한 페이지에 출력되는 상품 개수에 따라 DB Paging을 수행합니다.
    • (옵션) DB 트랜잭션과 동시성 제어를 고려합니다.
  3. 프론트엔드
    • 사용자 인터페이스 예시를 참고하여, 화면을 구성합니다.
    • API 명세에 따라 백엔드에 전달된 JSON 데이터를 필요에 따라 정돈하여 화면에 출력합니다.
    • 프론트엔드단에서 유효성 검사를 수행해야하는 지점을 고려합니다.
    • React.js 또는 Next.js를 기반으로 구현하며, 컴포넌트 단위로 구조를 설계합니다.
    • (옵션) 페이징 처리 시, 무한 스크롤을 고려합니다.

구현기능

Github 레포지토리 주소
위 프로젝트 정의서 중에서 장바구니, 헤더, 푸터 기능을 담당하게 되었다.

- 헤더, 푸터

각 페이지에 맞게 사용할 수 있도록 컴포넌트화

- 장바구니

  • 장바구니 목록 조회
  • 장바구니에 담긴 상품 데이터 (이미지, 상품명, 옵션 등)에 따른 상품별 구매 금액, 전체 주문 합계 금액 등을 화면에 출력
  • 지난 체크인 날짜, 재고 없음으로 인한 예약 마감 상품 표시
  • 장바구니 개별 삭제 기능 구현
  • 장바구니 체크 박스를 통해 삭제 기능 구현
  • 예약 불가 상품 삭제 기능 구현
  • 예약 마감 상품을 제외한 전체 선택 / 해제 기능 구현
  • 체크 박스를 통해 결제할 상품을 선택/제외 기능 구현
  • 장바구니에서 주문하기 버튼 클릭 시, 예약(주문) 페이지로 이동
헤더장바구니 개별, 선택 삭제
헤더g삭제g
예약 불가 장바구니 삭제전체 선택, 선택 항목 예약
예약 마감 삭제g예약하기g

에러 해결 방법

- 필요한 위치에서만 푸터 표시

NextJS 서버에서 푸터가 필요한 페이지인지 구분한 뒤에 렌더링이 되기 전에 푸터 유무를 판단하여 보여 주고 싶었는데 서버 컴포넌트의 header, cookie (from next/header)를 사용하여 정보를 받아와도 페이지를 판단할 수 있는 원하는 값을 찾을 수 없었다. 프로젝트 기한 때문에 필요한 페이지마다 푸터를 넣어주는 방식으로 임시 해결했지만 서버 컴포넌트와 클라이언트 컴포넌트의 차이에 대해서 공부할 수 있었다. 이후 리펙토링 과정에서 아쉬웠던 부분을 개선해보려고 한다.

- 장바구니 선택

장바구니에 예약 불가(체크인 날짜가 지났거나 예약 가능한 방의 수가 없는 경우) 항목은 체크가 불가능하게 처리, 전체 선택, 필요한 항목만 선택 후 삭제, 개별 삭제 등 고려해야할 경우의 수가 많아 많은 어려움이 있었다.

  • Strict 모드로 인한 전체 선택 배열에 같은 아이템이 들어가 실제 선택한 수의 2배가 선택 처리되는 이슈
  • 첫 렌더링 시 전체 선택이 될 때 각 checkbox의 onChange가 개별적으로 인식되지 않아 각 항목이 체크가 되었을 때 그에 따른 배열 값을 바꿔줘야하는 이슈
  • 이 외에도 많은 이슈가 있었지만 useEffectuseState 를 잘 고려하여 해결하면서 다시 리액트의 라이프 사이클을 공부할 수 있었다.

프로젝트 보안

1. 장바구니 목록 삭제 확인 절차없이 바로 삭제되는 문제

  • 상황
    장바구니 목록을 조회하는 페이지에서 '예약불가삭제', '선택삭제', '개별삭제' 버튼을 클릭하면 한번 더 확인하는 절차없이 바로 삭제되는 상황

  • 문제
    장바구니에 추가한 항목이 확인 절차없이 바로 삭제된다면 사용자 경험이 떨어지는 것으로 판단

  • 해결
    삭제를 확인하는 모달을 만들어서 바로 삭제되어 사용자 경험을 개선

  • 코드

    'use client';
    
     import { useEffect } from 'react';
     
     interface Props {
       title?: string;
       content?: string;
       cancel?: string;
       onCancelClick: VoidFunction;
       confirm?: string;
       onConfirmClick: VoidFunction;
     }
     
     const Modal = ({
       title = '삭제하시겠어요?',
       content,
       cancel = '아니요',
       onCancelClick,
       confirm = '삭제하기',
       onConfirmClick,
     }: Props) => {
       useEffect(() => {
         document.body.style.overflow = 'hidden';
         return () => {
           document.body.style.overflow = 'unset';
         };
       }, []);
     
       return (
         <div className='fixed left-0 top-0 z-50 flex h-screen w-screen items-center justify-center bg-[rgba(0,0,0,0.5)]'>
           <div className='w-[18.5rem] rounded-2xl bg-white px-4 pb-2 pt-8'>
             <div className='mx-1 mb-4 text-center text-base font-bold text-black'>
               {title}
             </div>
             <div className='text-gray1 mx-1 mb-5 text-center text-sm'>
               {content}
             </div>
             <div className='flex items-center justify-center text-base'>
               <button
                 className='text-gray1 mx-1 h-10 w-full flex-1'
                 onClick={onCancelClick}
               >
                 {cancel}
               </button>
               <button
                 className='text-blue mx-1 h-10 flex-1 font-bold'
                 onClick={onConfirmClick}
               >
                 {confirm}
               </button>
             </div>
           </div>
         </div>
       );
     };
     
     export default Modal;
    const DeleteButton = ({ cartId }: Props) => {
    const [isShowModal, setIsShowModal] = useState(false);
    
    return (
        <>
          <button
            type='button'
            aria-label='장바구니 삭제'
            onClick={() => setIsShowModal(true)}
          >
            <HiMiniXMark className='text-gray1' />
          </button>
          {isShowModal && (
            <Modal
              content='선택하신 상품이 삭제됩니다'
              onCancelClick={() => setIsShowModal(false)}
              onConfirmClick={deleteCartItem}
            />
          )}
         {isShowToast && <Toast message={isShowToast} />}
       </>
      );
    };
     
    export default DeleteButton;

    삭제 버튼을 클릭하면 모달을 표시해주고 항목을 삭제할지 한번 더 확인하는 절차를 거치도록 개선

2. api로 받아온 데이터를 사용하는 코드에 맞도록 전처리하는 코드가 길어 파일에 너무 많은 코드를 보유하고 있는 문제

  • 상황
    api로 받아온 장바구니 데이터가 화면에 보여줘야하는 형식과 차이가 있어 전처리를 해주어야하는데 그 코드가 너무 길어 한 파일에 너무 많은 내용을 가지고 있는 상황
  • 문제
    한 파일에 너무 많은 코드를 가지고 있어서 코드 가독성이 떨어지고 유지보수가 쉽지 않은 문제
  • 해결
    데이터를 전처리하는 코드를 커스텀훅으로 분리
  • 코드
     import { useEffect, useState } from 'react';
     
     import type { CartItemInfo, PreppedCartProduct } from '@/@types/cart.types';
     
     const useCartList = (apiCartList: CartItemInfo[]) => {
       const [preppedProductList, setPreppedProductList] = useState<
         PreppedCartProduct[]
       >([]);
     
       useEffect(() => {
         setPreppedProductList([]);
     
         apiCartList.map((item) => {
           setPreppedProductList((prevPreppedProductList) => {
             const existingIndex = prevPreppedProductList.findIndex(
               (prevPreppedProductItem) =>
                 prevPreppedProductItem.productId === item.product.productId
             );
     
             // 배열 안에 숙소가 존재하면
             if (existingIndex !== -1) {
               return prevPreppedProductList.map((prevPreppedProductItem, index) => {
                 // 숙소 안에 방만 추가
                 if (index === existingIndex) {
                   return {
                     ...prevPreppedProductItem,
                     cartRoomList: [
                       ...prevPreppedProductItem.cartRoomList,
                       {
                         id: item.id,
                         roomId: item.product.roomId,
                         imageUrl: item.product.imageUrl,
                         roomName: item.product.roomName,
                         baseGuestCount: item.product.baseGuestCount,
                         maxGuestCount: item.product.maxGuestCount,
                         price: item.product.price,
                         checkInTime: item.product.checkInTime,
                         checkOutTime: item.product.checkOutTime,
                         stock: item.product.stock,
                         checkInDate: item.checkInDate,
                         checkOutDate: item.checkOutDate,
                         numberOfNights: item.numberOfNights,
                         guestCount: item.product.guestCount,
                       },
                     ],
                   };
                 }
                 return prevPreppedProductItem;
               });
             }
     
             // 존재하지 않으면 숙소 및 방 추가
             return [
               ...prevPreppedProductList,
               {
                 productId: item.product.productId,
                 productName: item.product.productName,
                 address: item.product.address,
                 cartRoomList: [
                   {
                     id: item.id,
                     roomId: item.product.roomId,
                     imageUrl: item.product.imageUrl,
                     roomName: item.product.roomName,
                     baseGuestCount: item.product.baseGuestCount,
                     maxGuestCount: item.product.maxGuestCount,
                     price: item.product.price,
                     checkInTime: item.product.checkInTime,
                     checkOutTime: item.product.checkOutTime,
                     stock: item.product.stock,
                     checkInDate: item.checkInDate,
                     checkOutDate: item.checkOutDate,
                     numberOfNights: item.numberOfNights,
                     guestCount: item.product.guestCount,
                   },
                 ],
               },
             ];
           });
         });
       }, [apiCartList]);
     
       return preppedProductList;
     };
     
     export default useCartList;

회고

이전 토이2 프로젝트에서 익숙했던 페이지 라우터를 사용했었는데 이번 프로젝트에서 app 라우터를 사용하면서 app 라우터 개발 경험을 할 수 있었고 이전에는 고민하지 않았던 서버 컴포넌트와 클라이언트 컴포넌트에서 대해서 공부할 수 있었습니다.
백엔드와의 협업을 통해서 많은 개발이 진행되기 전에 빠르게 데이터 형식이나 api 문서를 통일한 뒤에 작업해야지 큰 문제 발생하지 않고 문제 해결도 수월하게 할 수 있다는 것을 알게 되었고 문서화와 소통의 중요성을 알게 되었습니다.
장바구니 기능 구현을 담당하면서 디테일한 작업들이 많아서 상태관리나 라이프 사이클을 공부할 수 있는 좋은 경험이 되었습니다. 코드의 가독성을 위해서 컴포넌트의 분리 및 컨벤션을 따르려고 노력했습니다. 팀원들과 대면으로 소통하여 원할하게 프로젝트를 마무리 할 수 있었습니다!

profile
프론트엔트 개발자입니다 🧑‍💻

0개의 댓글