[팀프로젝트] WE해요

minami·2022년 1월 10일
0

프로젝트

목록 보기
2/3

WE해요

요기요 사이트에 WE해요 팀만의 색깔을 넣어서 클론코딩을 하는 팀 프로젝트

  • 프로젝트 기간: 2021. 12. 27 - 2022. 01. 07 (약 2주)
  • 프로젝트 인원: 5명 6명
    - 프론트엔드: 3명
    - 백엔드: 2명 3명

1. 기획 단계

배민, 쿠팡이츠, 요기요 등 배달음식 플랫폼은 대부분 모바일 앱 기반이다. 그중에서도 요기요는 특이하게 웹사이트가 존재해서 웹사이트를 통해서도 배달음식 주문이 가능하게 되어 있었고, 일반적인 쇼핑몰과 형태가 좀 다를 뿐이지 대부분의 기능은 일반 쇼핑몰 사이트와 거의 유사하다. 그래서 첫 플래닝 미팅 때 나름대로 우리 팀이 구현해볼 수 있는 기능들을 최대한 구현하되 우리만의 너낌을 초큼 넣어보기로 했다.

특히, 기존 요기요 사이트의 UI가 우리 눈에는 별로 예뻐 보이지 않았기 때문에 우리 프론트엔드가 힘을 내서 바꿔보기로 했다. 그래서 오랜만에 카카오 오븐도 꺼내들고 와이어프레임을 열심히 뚝딱여보았다.

주요 기능 정의

  1. 식당 및 메뉴 검색 (메인 페이지)
  2. 카테고리별 식당 정렬 (목록 페이지)
  3. 별점 또는 리뷰 순 식당 정렬 (목록 페이지)
  4. 메뉴 보기 (상세 페이지)
  5. 리뷰 보기 (상세 페이지)
  6. 메뉴 옵션 선택 (상세 페이지)
  7. 위시 리스트
  8. 장바구니
  9. 결제

ERD

와이어프레임

위해요 와이어프레임 보러 가기

  • 테마 컬러: #000 👉 위코드 검정색!
  • 키 컬러: #fa0050 👉 요기요 테마색!
  • 배경색: #fafafa

2. 협업 방식

애자일 방법론의 SCRUM 방식!

그간 몇 번 교육도 들었고, 인턴을 빙자한 정직원 수준의생활도 좀 해봤다고 이번 팀 프로젝트에서 사용한 협업 방식과 툴 모두 나에겐 익숙한 것들이었다. 그래서 무난하게 금세 적응하여 팀 프로젝트를 할 수 있었던 것 같다.

  • SPRINT
    - 단위: 1주
    - 미팅
    1. 플래닝 미팅 (스프린트 단위)
    2. 데일리 미팅 (매일)
    3. 회고 미팅 (스프린트 단위)
  • 협업 툴
    • 트렐로
      • 스프린트 별 팀 전체 / 프론트엔드 or 백엔드 / 개인 별 작업 단위 설정
      • 리스트
        1. 전체필독사항
        2. Backlog
        3. This Week
        4. In Progress
        5. Done
    • 노션
      • 회의록 작성
    • 슬랙
      • 커뮤니케이션 툴
    • git & github
      • 버전 관리 툴

3. 구현 결과 - 상세 페이지

나는 자잘하게 기능이 여러가지가 있는 상세페이지를 맡았기 때문에 나름대로 필수 구현 기능 사항 외에도 추가 구현 기능 사항으로 남겨두었던 것들도 최대한 구현해보려고 노력했다. 그렇게 해서 구현한 결과들을 하나씩 살펴보자.

로딩 화면

데이터를 fetch해올 때 사용하는 useEffect는 모든 컴포넌트가 렌더링된 이후에 실행되어 부수효과를 일으킨다. 그래서 처음 페이지를 렌더할 때는 아직 불러온 데이터가 없는 상태이기 때문에 컴포넌트를 map으로 반복해서 불러올 수가 없다는 에러가 뜬다. 그리고 해당 에러 때문에 제대로 화면이 뜨지 않고 그냥 하얗게 에러가 난 상태 그대로 멈춰 버린다. 이런 상황을 방지하기 위해 조건부 렌더링을 사용할 수 있는데, 나는 거기에서 조금 더 나아가서 로딩 중임을 표시하고 싶었다.
그렇게 해서 찰나이지만 데이터를 불러오는 동안 로딩 화면을 표시할 수 있도록 조건을 걸어주었고, 실제 백엔드 서버와 통신하며 데이터를 불러오는 동안에 해당 로딩 화면이 이렇게 짠 하고 잘 뜰 수 있게 됐다!

아코디언 메뉴

요기요의 식당 상세페이지에는 해당 식당의 메뉴가 카테고리별로 나뉘어져 있다. 그리고 각 카테고리는 아코디언 메뉴로 표현되어 있기에 그걸 그대로 살려보고자 하였다.
각 아코디언 메뉴를 클릭했을 때 접혔다가 펼쳐졌다가 해야 하기 때문에 해당 기능은 아코디언 메뉴 컴포넌트에서 isMenuOpen이라는 state를 만들어서 true/false로 관리해주는 방식으로 구현했다.

이미지 슬라이더

대표 메뉴는 이미지 슬라이더로 구현했다.
초기에는 위스타그램 프로젝트 때 했던 것처럼 index를 사용해서 하는 것으로 했다가 그냥 true/falsestate를 관리해주는 방식으로 수정했다. 그래서 true일 때만 버튼이 나타나도록 하기가 수월했다. 이건 일단 대표메뉴 갯수가 7개로 고정되어 있었기 때문에 가능했던 일이다. 절대 너비 계산이 귀찮아서가 아니다!

탭 메뉴 - 메뉴와 리뷰

상세페이지에는 메뉴 탭과 리뷰 탭이 있어서 메뉴 탭에서는 각각의 음식메뉴를 볼 수 있고, 리뷰 탭에서는 식당의 전체 리뷰 평점을 비롯한 유저들의 리뷰를 볼 수 있게 되어 있다. 탭 전환은 간단하게 메뉴와 리뷰에 각각 인덱스를 부여해서 메뉴를 클릭하면 0, 리뷰를 클릭하면 1이 선택되게 하였다. 그래서 선택된 인덱스 숫자에 따라 메뉴 컴포넌트 또는 리뷰 컴포넌트가 보이도록 해서 간단하게 구현할 수 있었다.
또한, 리뷰는 백엔드에서 보내준 정보들을 넣기만 하면 되었기 때문에 각각의 리뷰 컴포넌트를 반복적으로 보여주도록 하는 간단한 원리를 적용할 수 있었다.

모달

이번 프로젝트에서 내가 가장 힘을 준(🙃...) 부분이 바로 요 모달이다. 요 쪼끄만 모달에 들어가는 기능들이 어찌나 많은지ㅎ... 아이, 즐거워.

  1. 옵션 라디오 버튼
    • 선택한 옵션의 가격을 총 주문 가격에 바로 더할 수 있도록 구현
  2. 수량 조절
    • 해당 메뉴의 기본값을 1로 고정해두고 1 미만으로는 선택할 수 없으며 증감버튼으로 수량 조절
  3. 총 주문 가격
    • 수량을 조절할 때마다 선택된 수량만큼 메뉴 가격을 더하고, 옵션 선택시 옵션의 가격까지 더하여 총 주문 가격 표시
  4. 모달 바깥 영역 처리
    • 모달이 열리면 모달 바깥 영역의 스크롤 불가 처리
    • 모달 바깥 영역 클릭시 모달 닫기
    • 취소하기 또는 X 버튼 클릭시 모달 닫기
  5. 주문 완료 처리
    • 주문하기 버튼 클릭시 주문 완료 Toast 메시지 처리

모달 하나에만 이렇게 자잘하게 많은 기능들이 들어갈 줄은 직접 구현해보기 전까지 몰랐던 것 같다. 상술한 기능 외에도 컴포넌트 재사용성을 살리고자 노력한 부분이 있는데 그건 다음 4번에서 설명하겠다.

4. 프로젝트 후기

😎 잘했다고 생각하는 점

1. children 사용

모달 컴포넌트 children 사용기

이번에 내가 상세 페이지를 담당하면서 가장 힘을 준 부분인 모달.
모달은 내용 컨텐츠 부분만 다르고, 나머지 부분과 레이아웃은 모두 동일하기 때문에 재사용성을 최대한 높여보기로 마음 먹었다. 그래서 모달 컴포넌트의 재사용성 높이기 일환의 첫 번째로 children을 사용해 보았다.
그리하여 나온 것이 바로 ModalLayout.jsModal.js인데 각 컴포넌트의 내용은 다음과 같다.

ModalLayout.js
// ...생략...
  return (
    <>
      <div className="overlay" onClick={closeModal} />
      <div className="modal_layout">
        <div className="modal_header">
          {/* 생략 */}
        </div>
        {children}
        <div className="modal_footer">
          <button type="button" onClick={closeModal}>
            취소하기
          </button>
          <button type="button" onClick={submitMenuOrder}>
            장바구니에 담기
          </button>
        </div>
      </div>
      {isActiveToast && <Toast />}
    </>
  );
}
Modal.js
//...생략...
  return (
    <div>
      {isModalOpen && modalContent && (
        <ModalLayout>
          <div className="modal_content">
            <div className="menu">
              <div className="image_container">
                <img
                  alt={modalContent.menu_title}
                  src={`${modalContent.menu_image}`}
                />
              </div>
              <div className="title">{modalContent.menu_title}</div>
              <div className="price">
                <p>가격</p>
                <p>{modalContent.menu_price}</p>
              </div>
            </div>
            <div className="radio_options">
              <ul>
                {/* 생략 */}
              </ul>
            </div>
            <div className="amount">
							{/* 생략 */}
            </div>
            <div className="total_price">
              <p>총 주문금액</p>
              <p>{calculateTotalPrice(modalContent.menu_price)}</p>
            </div>
          </div>
        </ModalLayout>
      )}
    </div>
  );
}

ModalLayout.js에 비해 아무래도 Modal.js의 코드가 아주 길어서 중간중간 생략을 했는데도 여전히 길다. 그만큼 모달 내용 컨텐츠 부분에 들어갈 것들이 많았는데 이렇게 모달 레이아웃 컴포넌트와 모달 컴포넌트를 분리하지 않았더라면 가독성 측면에서도 훨씬 복잡해서 어려웠을 것 같다.

2. 커스텀 훅(Custom Hooks) 사용

useModal 사용기

리액트로 개발하면서 커스텀 훅을 사용해본 건 이번에 처음이었는데, 그래서일까...? 머리를 몇 번이나 쥐어뜯었는지🙃...
열심히 구글링하면서 적용해본 것이라 사실 정확한 작동원리나 세세한 부분에 대해서는 아직 이해도가 좀 부족하다. 하지만 무사히 모달 컴포넌트에 커스텀 훅을 적용해서 잘 작동하게 했다는 것이 지금은 중요한 것 같다.

커스텀 훅을 적용하게 된 이유는, 상세 페이지의 음식 메뉴 하나하나마다 모두 클릭 이벤트가 발생하면 해당 메뉴의 가격과 추가 가능한 옵션 및 수량 조절 등이 가능한 모달 창을 띄워줘야 하기 때문이다. 그렇게 하려면 내가 설계한 레이아웃 상 모달 컴포넌트가 대표 메뉴 컴포넌트와 카테고리별 메뉴 컴포넌트에서 각각 다 사용이 가능해야 했다. 그리고 대표 메뉴와 카테고리별 메뉴 컴포넌트는 또 Menus.js라는 상위 컴포넌트 아래에 있는 형제 컴포넌트들이었고, 나는 모달 컴포넌트를 그렇게 하위 컴포넌트마다 일일이 다 import해서 쓰도록 하고 싶지 않았다. 다시 말해서 재사용성을 높이고 싶었다.

방법을 찾아보다 보니 커스텀 훅이라는 것으로 모달을 전역에서 사용할 수 있도록 해주는 방법이 있다는 걸 알게 되었고, 그렇게 고난의 길로... 커스텀 훅 구현을 시도해보았는데 성공했다!

useModal.js
import { useEffect, useState } from 'react';

export default () => {
  let [isModalOpen, setIsModalOpen] = useState(false);
  let [contentId, setContentId] = useState(0);
  let [modalContent, setModalContent] = useState([]);

  let openModal = contentId => {
    setIsModalOpen(true);
    setContentId(contentId);
  };

  let closeModal = () => {
    setIsModalOpen(false);
  };

  useEffect(() => {
    fetch('/data/RestaurantDetail/menudetail.json')
      .then(res => res.json())
      .then(result => setModalContent(result));
  }, [contentId]);

  return { isModalOpen, openModal, closeModal, modalContent };
};
modalContext.js
import React, { createContext } from 'react';
import Modal from './Modal/Modal';
import useModal from './useModal';

let ModalContext;
let { Provider } = (ModalContext = createContext());

let ModalProvider = ({ children }) => {
  let { isModalOpen, openModal, closeModal, modalContent } = useModal();
  return (
    <Provider value={{ isModalOpen, openModal, closeModal, modalContent }}>
      <Modal />
      {children}
    </Provider>
  );
};

export { ModalContext, ModalProvider };

useModal.js에서는 Provider를 통해 이용할 수 있게 할 함수나 state 같은 것들을 작성해주고, modalContext.jsProvider를 이용해서 Modal을 Provider로 감싼 곳 내부에 한해 어디서든 사용할 수 있도록 하는 컨텍스트를 작성해주었다.
뒤늦게 알게 된 것인데 컨텍스트는 문맥, 맥락이라는 뜻을 갖고 있는 만큼 어디서든 사용할 수 있게 해주는 것이었다.

그렇게 앞서서 모달 레이아웃을 활용한 모달 컴포넌트와의 결합을 통해 재사용성을 높이자는 목적을 달성할 수 있었다.

3. 함수 별도 분리 사용

별 컴포넌트 렌더링 함수 분리하기

각 식당별 전체 평점과 유저의 후기별 평점에 따라 꽉 찬 별, 반만 찬 별, 빈 별 세 가지 종류의 별을 렌더링하고 싶었다. 세 종류의 별은 react-icons 라이브러리를 사용해서 가져왔다. 그리고 평점에 따라 보여줄 꽉 찬 별의 갯수, 반만 찬 별의 갯수, 빈 별의 갯수를 계산하여 렌더링해주도록 하는 함수를 작성해서 렌더링하는 데까지는 성공을 했다. 그런데 문제는 컴포넌트화된 세 종류의 별을 렌더링하는 함수가 상세 페이지 내의 여러 컴포넌트에서 모두 동일하게 쓰인다는 점이었다. 즉, 이번에도 재사용성의 문제가 있었다.

상세 페이지 레이아웃상 별이 렌더링되어야 하는 컴포넌트는 총 3군데였다.
1. 가게 정보 컴포넌트
2. 가게 전체 후기 평점 컴포넌트
3. 각 유저의 후기 컴포넌트

이 세 군데 컴포넌트에 모두 동일한 별점 렌더링 함수를 각각 작성해준다면 그것만큼 비효율적인 게 없다. 그래서 멘토님의 조언을 받아서 utils라는 디렉토리를 src 디렉토리 안에 생성하고 RenderStars.js라는 파일도 하나 만들었다. 그리고 그 파일의 내용으로 renderStars라는 함수를 작성해서 export하여 해당 함수가 필요한 컴포넌트에서 바로 import하여 사용할 수 있게 했다.

utils 디렉토리

😥 아쉬웠던 점

1. 초기 기획과 달리 구현하지 못한 기능들

초기 기획에는 장바구니와 위시리스트, 결제 페이지까지 3개의 페이지가 더 존재했었다. 그런데 다 구현하지 못한 것이 제일 아쉽다. 무엇보다도 정말이지 많은 일들이 2주도 채 안 되는 시간 동안 벌어졌다. 팀원 한 명이 중도하차를 한 데다 다른 팀원이 코로나19에 확진되면서 시간적인 여유가 많이 줄어들었던 것. 어쩔 수 없이 일정과 기획을 조정해야만 했다. 그래도 모든 팀원들이 할 수 있는 한 최선을 다해서 임했기 때문에 프로젝트를 완성할 수 있었다!

2. 리팩토링 부족

생각보다 시간이 빠듯했어서 리팩토링까지는 다 할 수 없었던 부분이 아쉽다. 하지만 리팩토링은 나중에라도 내가 계속 할 수 있는 부분이니까 또 새로운 팀원과 함께 해야 하는 2차 프로젝트와 기업 협업 등 앞으로 남은 일정들을 소화하면서 천천히 해보려고 한다.

5. 프로젝트 소스

WEHAEYO 프론트엔드 레포지토리
WEHAEYO 백엔드 레포지토리

profile
함께 나아가는 개발자💪

0개의 댓글