✪ 취미, 경험 회고 및 일상/[회고] IT 관련 경험 회고

위코드 1차 팀 프로젝트 'HealthEat' 회고

SangYoonLee (SYL) 2022. 11. 27. 16:42
반응형

프로젝트 소개

 

팀 소개

  • 프론트엔드 3명
    • 우석민 : Nav 바, 스토어 페이지 구현
    • 이상윤(나) : 메인 페이지, 상품 상세 페이지 구현
    • 이혜원 : 로그인 페이지, 회원가입 페이지 구현
  • 백엔드 2명
    • 이은영 : 찜하기 기능 구현
    • 조상원(PM) : 로그인, 제품조회 기능 구현

 

 

개발 도구 및 적용 기술 (+트렐로)

  • 프론트엔드
    • JavaScript(ES6)
    • React.js
    • Sass
    • React-router-dom
    • Fontawesome
  • 백엔드
    • JavaScript(ES6)
    • Node.js
    • Express
    • JSON Web TOKEN
    • Bcrypt
    • My SQL
    • Multer
  • 커뮤니케이션 및 버전 관리
    • Slack
    • Trello
    • Postman
    • Git / Github

 

작업 결과 (시연 영상)

 

 

백엔드 DB 모델링 및 END POINT

 

 

나의 작업 (내 구현 사항)

메인 페이지의 화면 슬라이더 구현

// PromotionSlide.js

// ... import 코드 생략

const PromotionSlide = () => {
  const [currentSlide, setCurrentSlide] = useState(0);
  const [sliderData, setSliderData] = useState([]);
  const slideLength = sliderData.length;

  // ... 데이터를 받아오는 코드 생략

  useEffect(() => {
    setCurrentSlide(0);
  }, []);

  // 5.5초 간격으로 다음 사진 슬라이드를 불러오는 auto 함수
  const auto = () => {
    slideInterval = setInterval(nextSlide, intervalTime);
  };

  let slideInterval = null;
  let intervalTime = 5500;

  useEffect(() => {
    auto();
    return () => clearInterval(slideInterval);
  }, [auto, currentSlide, slideInterval]);

  // 다음 사진 슬라이드로 넘어가는 함수
  const nextSlide = () => {
    setCurrentSlide((currentSlide + 1) % slideLength);
  };

  // 이전 사진 슬라이드로 넘어가는 함수
  const prevSlide = () => {
    setCurrentSlide((currentSlide - 1 + slideLength) % slideLength);
  };

  return (
    <article className="promotion-slide">
      {sliderData.map((slide, index) => {
        return (
          <div
            className={index === currentSlide ? 'slide current' : 'slide'}
            key={index}
          >
            {index === currentSlide && <img src={slide.image} alt="slide" />}
          </div>
        );
      })}
      
    // ...
    // 생략
    // ...
      
    </article>
  );
};

export default PromotionSlide;

 

 

 

메인 페이지의 카테고리 리스트 구현 및 각 항목에 알맞은 제품을 표시할 수 있도록 스토어 페이지로 연결

// MenuList.js

// ... import 코드 및 상수 데이터 코드 생략

const MenuList = () => {
  return (
    <article className="menu-list">
      <div className="menu-list-title">
        <span className="menu-list-title-name">고민별 상품 보기</span>
      </div>
      <div className="menu-list-items">
        {MENU_LIST_DATAS.map((data, idx) => (
          <ListItem key={idx} menuListDatas={data} />
        ))}
      </div>
    </article>
  );
};

export default MenuList;
// ListItem.js

import React from 'react';
import { useNavigate } from 'react-router-dom';

const ListItem = ({ menuListDatas }) => {
  const { category, name, image } = menuListDatas;
  const navigate = useNavigate();

  return (
    <div className="menu-list-each-item">
      <div
        className="menu-icon"
        onClick={() => {
          if (category >= 1 && category <= 4)
            navigate(`/store?category=${category}`);
        }}
      >
        <img src={image} />
      </div>
      <p className="menu-name">{name}</p>
    </div>
  );
};

export default ListItem;

 

 

 

백엔드에서 제품의 상세 데이터를 받아 상품 디테일 페이지에 표시 + 구매 수량에 맞춰 상품 가격 변경

// 구매 수량에 맞춰 상품 가격이 변하도록 구현한 코드

// BuyBar.js

// ... import 코드 생략

const BuyBar = ({ productID, productData }) => {
  const [quantity, setQuantity] = useState(1);
  const [totalPrice, setTotalPrice] = useState(0);
  const navigate = useNavigate();

  const { name, information, brand_name, price, discount_rate } = productData;

  useEffect(() => {
    setTotalPrice(parseInt(price) * quantity);
  }, [price, quantity]);

  // 조건부 랜더링
  // : 첫 랜더링 시 (랜더링 이후 실행되는) useEffect의 특성으로 인해 백엔드로부터 데이터가 수신되지 않으므로
  // 작성한 코드이다. 
  if (totalPrice === 0) return null;

  const plusQuantity = () => {
    setQuantity(prev => prev + 1);
  };

  const minusQuantity = () => {
    // quantity(구매 수량)가 0 이하로 내려가지 않도록 작성한 방어 코드 
    if (quantity >= 2) {
      setQuantity(prev => prev - 1);
    }
  };

  return (
    <>
      
      // .. 생략
      
        <div className="product-price">
          <span className="price discounted-price">
            {`${Math.round(totalPrice * (1 - discount_rate)).toLocaleString(
              'ko-KR'
            )}원`}
          </span>
          <span className="price fixed-price">
            {`${Math.round(totalPrice * 1).toLocaleString('ko-KR')}원`}
          </span>
          <span className="price discount-rate">{`${
            discount_rate * 100
          }%`}</span>
        </div>
        
      // .. 생략

    </>
  );
};

export default BuyBar;

 

 

프로젝트 후기 및 느낀 점

첫 팀 프로젝트를 통해 배웠던 것들

  • 브랜치를 생성해서 서로 작업하는 도중 초기 세팅을 수정해야 할 일이 생겼을 때 해결 방법
    • 다른 팀원에게 영향이 가는 수정 사항일 경우 새로운 브랜치를 생성하여 수정 사항을 반영한다.
    • 다른 팀원에게 영향이 가지 않는 수정사항일 경우 자신이 작업하고 있던 브랜치에 수정 사항을 반영한다.
  • 브랜치에서 다른 브랜치로 이동하고자 할 때, 현재 브랜치의 작업 내역을 모두 커밋한 후 이동해야 작업 내역이 꼬이지 않는다.

 

  • 브랜치 이름 작성 방식
    • 보통 branch명은 camelCase로 작성한다.
    • 예시 : feature/productDetail
  •  브랜치 기능 별 라벨 명
    • feature : 기능 구현할 때
    • develop : 여러 기능을 합쳐서 테스트할 때
    • release : 배포 전 최종 테스트할 때
    • hotfix : 배포 후 급하게 버그를 수정할 때

 

  • 팀 내 컨벤션은 미리 정해두는 것이 좋다. 
    • (예시) Git Branch 이름은 컴포넌트 이름과 동일하게 짓되, camelCase로 작성합니다.
    • (예시) CSS 클래스 이름은 kebab-case 방식으로 작성한다. 이름은 자유롭게 짓되, 이름을 보고 그것이 어떤 태그를 가리키는 지 파악할 수 있는 이름으로 작성합니다.
  • Daily Standup Meeting의 목적과 잘하는 법
    • 기한 내에 제품을 개발할 수 있도록 생산성을 높이기 위해서 Daily Standup Meeting을 진행한다.
    • 최대한 짧고 간결하게, 내가 한 것, 앞으로 해야할 것, 현재 막힌 부분 (문제 사항) 서로 공유하기
    • 미리 준비해오기

 

잘했던 점

  1. 팀원 모두가 서로에 대한 배려와 존중을 기반으로 상대방의 의견을 잘 들어주고 소통한 점
  2. 상황과 상관없이 함께 목표를 달성하겠다는 의지를 보여준 점
  3. 프로젝트를 진행하며 Notion, Figma 등 필요한 도구들을 바로바로 적용한 점
  4. 백엔드와 프론트엔드끼리 서로의 지식을 공유하고 배우려 한 점

 

아쉬웠던 점

  1. 스프린트 때 자세하게 티켓을 나누지 못한 점 (티켓의 내용을 세분화를 하지못하고 스토어, 상세페이지 등 크게 파트만 나누었다)
  2. 각자 자신의 업무에 몰입하느라, 모르고 어려운 것들(blocker)에 대해 팀원들과 공유하고 질문하는 것이 잘 되지 않았던 점
  3. 팀원 중 코로나 환자가 발생하여 온라인으로 미팅을 진행해야 했던 경우 서로 소통의 어려움이 있었던 점 (예: 온라인 참여자의 존재를 잠시 잊어버리고 오프라인 참여자들끼리만 대화를 이어나갔던 점)
  4. BE와 FE가 너무 분리되어 진행되다 보니 파트별 진행 상황을 서로 잘 알지 못했던 점
  5. 스프린트를 정보 공유의 목적으로만 진행해서 시간관리가 잘 되지 않았던 점
  6. 시간 관리를 위해 공통된 목적에 대한 데드라인을 정하지 않았던 점
  7. Fontawesome 같은 외부 라이브러리의 사용을 팀원들에게 깜박하고 미리 말하지 않은 채 내 클라이언트에만 설치하여 사용했던 점 (나중에 서로의 작업물을 main 브랜치에 merge하여 테스트를 진행할 때 충돌이 발생했다)
  8. 브랜치명, 클래스명, 변수명 컨번션을 미리 정하지 못하고 프로젝트 도중 정해 팀원들과 잘 공유되지 못했던 점

 

다음 프로젝트 때 개선할 포인트

  1. 스프린트 회의 때 시간이 좀 걸리더라도 최대한 자세하게 티켓을 세분화하여 전체적인 큰 그림을 모든 팀원들이 잘 이해한 상태로 프로젝트를 진행할 수 있게끔 하자.
  2. blocker들을 기록하고, 그 blocker에 대한 데드라인을 정해 프로젝트가 일정에 맞게 진행될 수 있도록 하자.
  3. 정확한 시간대를 정하여 프로젝트 진행하자.
  4. 개개인의 체력 및 컨디션 관리. 영양제도 좋지만, 기초 체력은 운동에서 나온다!
  5. 첫 번째 미팅 때 기획을 최대한 세분화해서 명확하게 잡아야 한다! (BE & PE 파트너를 정해서 하나의 기능 구현을 같이 진행하는 것도 좋은 방법)
  6. PM은 역할을 제대로 파악하여 계획한 일정들이 미뤄지지 않도록 힘써야한다.
  7. 팀원 각자 책임감을 갖고 서로 도울 수 있는 부분을 생각해보자.
  8. 데이터 형태를 어떻게 보내고 받을건지 → ERD를 함께 만들어보면 좋을 것 같다.
  9. 구글 드라이브 또는 노션에 필요한 데이터, 이미지등을 정리하는 공간을 만들어두면 좋을 것 같다.

 

느꼈던 점

팀 프로젝트가 처음이다 보니, 시작부터 모든 것이 낯설고 어색하게 다가왔다. 나만 그런 것이 아니라 모든 팀원 분들이 다 그렇게 느꼈으리라 생각한다. 하지만 그렇기에 프로젝트를 통해 배운 것 또한 많지 않았나 싶다. 고맙게도 처음 프로젝트를 기획할 때부터 모든 팀원 분들이 적극적으로 의견을 내주셨고, 또 서로 배려하는 자세를 갖고 적극적으로 프로젝트에 임해주셔서 나 또한 프로젝트에 더더욱 열심히 참여하려 열정을 낼 수 있지 않았나 싶다.

 

 

프로젝트 중 잘했던 점을 생각해보면, 같은 프론트엔드 팀원들을 적극적으로 도와주었다는 점을 꼽을 수 있을 것 같다. 당시 팀원 중 개발 관련 경험이 내가 제일 많아서 그랬는지, 팀원들이 개발 도중 어려운 일이 생기면 나에게 먼저 질문을 많이 해주셨다. 그래서 그럴 때마다 적극적으로 팀원을 도와가며 프로젝트가 중간에 막히지 않도록 신경썼다.

 

또 하나 잘했다고 생각이 드는 점을 기록하면, 프로젝트 중 모르는 것이 생겨 멘토님께 질문을 하기 전, 혼자 우선 고민을 해본 후 질문할 내용을 미리 정리했다는 점과, 개발 도중 만났던 오류를 그냥 넘기지 않고 따로 정리했다는 점이다. (내 깃허브의 ERROR NOTE 저장소는 이렇게 탄생하게 되었다.)

 

반면 아쉬웠던 점 역시 있었다. 특히 프로젝트 중 내 실수로 팀원들에게 미안해할만한 일이 생겼던 적이 있었다. 위에서도 언급을 했지만, FontAwesome 패키지를 내 로컬에만 설치하여 프로젝트에 활용했고 이를 팀원들에게 깜박하고 말하지 않았던 것이다. 결국 이로 인해 최종 테스트 때 패키지 충돌이 발생하였고, 팀원들에게 당혹감을 주고 나서야 비로소 '아차' 싶었다. 이번 일을 통해 팀원들과 꼭 공유해야 할 내용은 그때그때 바로 말해서 앞으로는 이런 일이 생기지 않도록 조심해야겠다는 생각이 들었다. 좋은 경험을 했다.

 

 

사실 프로젝트 시작 전, 2주도 채 되지 않는 짧은 기간 안에 프로젝트를 완성하는 것이 가능할까 싶은 생각이 강했다. 심지어 팀원 분들이 돌아가며 코로나에 걸리는 바람에 더욱 프로젝트 완성에 대한 확신이 부족했다. 그렇지만 이런 어려운 상황 속에서도 다들 맡은 바 최선을 다해준 덕분에 큰 문제없이 기간 내에 무사히 프로젝트를 마무리할 수 있었다.

비록 배포까지 하지 못한 점과 최종 결과물에 대한 아쉬움이 남는 것은 사실이지만, 기능 구현보다는 팀 프로젝트를 통해 최대한 다양한 경험을 해보자는 마인드여서 그랬는지, 충분히 만족스러운 프로젝트였다고 말할 수 있을 것 같다. 


마지막으로 이 자릴 빌어 함께 고생해준 팀원분들 모두에게 고맙고 고생했다는 말 남기고 싶다.

 

반응형