[회고] 생애 첫 리액트 프로젝트를 돌아보며

sujin·2022년 10월 2일
2
post-thumbnail

짧으면 짧고 너무 짧다면 너무 짧았던 2주가 드디어 끝이 났다. 사실 첫 일주일은 이게 과연 끝이 나긴 할까..? 라는 막막한 생각이 많이 들었다면 2주차에는 마음이 급격히 조급해 지기 시작했던 것 같다. 그래도 별일 없이 프로젝트를 무사히 마친 팀원과 나 자신에게 수고했다고 말하고 싶고 어느때보다 치열하고 고통스러웠던 나의 2주를 돌아보려고 한다.

정육쩜을 아시나요?

우선 팀원 부터 소개를 하자면 ,

  • BE : 재호님, 우진님
  • FE : 주안님, 효성님, 수진님(🙋🏻‍♀️)

이렇게 오순도순 다섯명으로 출발했다. 다른 팀에 비해 인원이 프론트,백엔드 1명씩 없었기 때문에 초반에는 괜찮을까? 라는 걱정이 있었지만 우리의 팀원들은 손이 빨랐고 백엔드 역시 두 분이 세명의 몫을 충분히 해내셨다.

우리 팀은 정육각 사이트를 클론코딩 했는데, 정육각에 대해서 간단히 소개하자면 육류를 주력을 판매하는 쇼핑몰이다. 사실 이 아이디어는 내가 낸 아이디어였는데 어떻게 하다보니 운이 좋게(?) 1차 프로젝트로 선정이 되었다.

내가 정육각을 선정한 이유우선 UI가 단순하고 깔끔해서 기능 구현에 집중할 수 있을 것 같다는 생각을 했고 첫 프로젝트로 하기에 부담이 없을 것 같았다.

먼저 팀원들과 함께 간단하게 사이트 분석을 마치고 필수 구현 페이지추가 구현 페이지로 나누어 구현할 페이지를 선정했다.

  • 필수 구현 페이지
    • header/footer, 회원가입/로그인, 리스트, 상세, 장바구니, 주문하기
  • 추가 구현 페이지
    • 메인(캐러셀), 리뷰, 마이페이지

이 중에 나는 header footer 장바구니 를 맡아서 진행하게 되었다.


내가 PM이라니?

정육쩜의 아이디어가 내가 낸 것이다 보니 자연스럽게 내가 PM이 되었다.
PM이라는 것이 낯설기도하고 어색하기도 했지만 프로젝트를 잘 끝내고자하는 욕심이 있었고 무엇보다 팀에 도움이 되는 PM이 되고 싶었기 때문에 팀을 잘 이끌어 보고자 노력을 했던 것 같다.

우리 팀은 진행상황을 Trello 로 서로 공유했고 회의를 할때 말로만 하다보니 회의 내용이 잘 정리가 되지 않아 소통의 어려움을 겪을 때가 있어 서로 공유해서 보는 화면이 있으면 좋을 것 같다는 생각이 들었고 Figma를 사용해서 구현할 레이아웃을 간단히 정리했다.


지옥의 장바구니

내가 맡은 header 와 footer는 사실상 레이아웃을 짜는 거라서 특별히 어려운 기능이 있지는 않아서 빨리 끝낼 수 있었다.

그래서 다른 팀원 들보다 빨리 끝내고 장바구니를 들어가게 되었는데 그때부터 나의 시련은 시작 되었을지도 모르겠다...

정육각의 장바구니에는 상품을 선택하는 기능이 따로 없었다. 그래서 나는 체크박스 기능을 추가해서 선택 삭제, 선택 주문 기능을 추가로 넣고 싶었고 백엔드와 이야기를 해본 결과 가능할 것 같다 그래서 진행하게 되었고, 기능 구현을 했던 것 중에 어려웠고 기억에 남는 코드 몇가지를 소개해보려고 한다.

체크 박스 구현

체크 박스 기능은 총 3가지 조건을 만족해야한다.

  • 전체 체크 박스 선택개별 체크 박스 모두 선택 되야함
  • 개별 체크 박스 모두 선택전체 체크 박스 선택 되어야함
  • 개별 체크 박스가 하나라도 해제되면 전체 체크 박스도 해제 되어야함

내가 생각한 로직은 선택된 상품의 id값을 관리하는 state와 상품 전체를 관리하는 state를 생성해서 그 둘을 비교하면서 체크 박스의 상태를 관리하는 것이었다.

//Cart.js

const Cart = () => {
  const [cartItem, setCartItem] = useState([]); // 장바구니에 담긴 상품
  const [checkedItem, setCheckedItem] = useState([]); // 선택된 상품
  
  const isAllChecked = checkedItem.length !== 0 && cartItem.length === checkedItem.length;

  const handleSingleCheck = id => {
      if (checkedItem.includes(id)) {
        setCheckedItem(checkedItem.filter(el => el !== id));
      } else {
        setCheckedItem(checkedItem.concat(id));
      }
    };

  const handleAllCheck = () => {
      if (isAllChecked) {
        setCheckedItem([]);
      } else {
        setCheckedItem(cartItem.map(({ id }) => id));
      }
   }
  
  const getCartData = () => {
    fetch(`${API.CART}/user`, {
      headers: {
        'Content-Type': 'application/json;charset=utf-8',
        Authorization: localStorage.getItem('token'),
      },
    })
      .then(res => res.json())
      .then(data => setCartItem(data.getCartbyId)); 
  };

  useEffect(() => {
    getCartData();
  }, []);
  
  return(
	...
    <div className="item_table">
              <div className="item_header">
                <div className="check_area">
                  <input
                    type="checkbox"
                    id="checkAll"
                    title="선택"
                    checked={isAllChecked}
                    onChange={handleAllCheck}
                  />
                  <label htmlFor="checkAll" />
                </div>
                <p className="header_title">상품정보</p>
                <p className="header_title">수량</p>
                <p className="header_title">가격</p>
              </div>
              <ul className="item_list">
                {cartItem.map(data => (
                  <CartItemList
                    key={data.id}
                    itemInfo={data}
                    handleSingleCheck={() => handleSingleCheck(data.id)}
                  />
                ))}
              </ul>
              <button className="all_delete" onClick={deleteProduct}>
                선택 상품 삭제
              </button>
            </div>

  )		

}

export default Cart;

위에 코드에 대해서 간단히 설명을 하자면,

  • checkedItem 배열의 길이과 cartItem의 길이가 같다면 true값을 반환하는 변수(isAllChecked)를 선언한다.
  • 변수 isAllChecked 를 전체 체크 박스를 관리하는 input의 checked={isAllChecked} 로 넣어준다.
  • 전체 체크박스를 관리하는 함수 handleAllCheck를 만들고 check의 상태값을 인자로 받는다.
    • 그래서 만약 check 값이 true 라면 setChekcedItemcartItem에 담긴 데이터들의 id 값들을 넣어 준다.
    • check 값이 false 라면 setChekcedItem에 빈배열을 넣어주어 값을 초기화 시켜준다.
// CartItemList.js

const CartItemList = ({
  itemInfo,
  checkedItem,
  handleSingleCheck,
}) => {
  const {
    id,
    option_products_id,
    tumbnail_url,
    name,
    standard_unit,
    price,
    thick,
    quantity,
  } = itemInfo;


  return (
    <li>
      <div className="check_area">
        <input
          type="checkbox"
          id={`check${id}`}
          title="선택"
          checked={checkedItem.includes(id)}
          onChange={handleSingleCheck}
        />
        <label htmlFor={`check${id}`} />
      </div>
      <img src={tumbnail_url} alt="sample" />
      <div className="product_info">
        <p className="name">
          {name}
          <span className="option">{standard_unit}</span>
        </p>
        <span className="standard">{thick}g 기준</span>
      </div>
      <div className="amount_box">
        <button className="box" onClick={minusQuantity}>
          <i className="fa-solid fa-minus" />
        </button>
        <p className="box">{quantity}</p>
        <button className="box" onClick={plusQuantity}>
          <i className="fa-solid fa-plus" />
        </button>
      </div>
      <p className="price">{(price * quantity).toLocaleString()}</p>
    </li>
  );
};

export default CartItemList;
  • CartItemList 컴포넌트는 장바구니의 상품 리스트를 나타내는 컴포넌트로 여기에는 상품 개별 체크박스가 있다.
  • 개별 체크 박스들을 관리하는 함수 handleSingleCheck()를 만들고 props로 전달 받는다.
  • 이때, check의 상태 값과 id값을 함수의 인자로 받는다.
    • 만약 check 값이 true라면 setChekcedItem에 id값을 쌓고
    • check 값이 false라면 checkItemfilter()를 이용해서 id 값이 다른 요소들(체크되지 않은 상품의 id)을 걸러준다.

선택한 상품만 합계 금액에 나타내기

선택한 상품의 합계 금액은 원래 처음에는 for문을 사용했었는데 평소에 잘 사용하지 않았던 reduce를 사용하여 리팩토링 했기 때문에 기억에 남는 코드 중 하나이다.

const [cartItem, setCartItem] = useState([]);
const [checkedItem, setCheckedItem] = useState([]);

//변경 전
const totalPrice = () => {
 let price = 0;
 for (let i = 0; i < checkedItem.length; i++){
 	price += cartItem[i].price * cartItem[i].amount;
 }
 reuturn price;
}

// 변경 후
const totalPrice = cartItem.reduce(
  (acc, cart) =>
    checkedItem.indexOf(cart.id) !== -1
      ? acc + Number(cart.price) * cart.quantity
      : acc,
  0
);

  • 구현 동작 : 선택한 상품의 가격만 더해서 최종 합계 금액에 보여주려고 한다.
  • reduce함수를 간단히 설명하자면, 배열의 각 요소를 순회하면서 설정한 함수를 실행시키며, 반환 값으로 누적계산의 결과 값이 나오게 된다. 그래서 cartItem(=상품 정보)를 순회 하면서 checkedItem(=선택된 상품)의 id값을 찾아서 그 값이 있다면 가격과 수량을 곱해서 최종 합계 금액을 구할 수 있었다.

useNavigate()로 state 전달하기

평소에 useNavigate는 페이지 이동할 때만 사용하는 hook인 줄 알았는데 장바구니에서 선택한 상품을 주문하기로 넘어갈 때 useNavigate를 사용하여 선택한 상품의 id값을 state로 주문하기 페이지로 넘겨줄 수 있다는 사실을 새롭게 알게 되어 기억에 남는다.

//Cart.js
import { useNavigate } from 'react-router-dom;

const navigate = useNavigate();
  const postOrder = () => {
    navigate('/payment', { state: { cartId: checkedItem } });
};

//Payment.js
import { useLocation } from 'react-router-dom';
const location = useLocation();
const { cartId } = location.state;

마무리

끝나지 않을 것 같았던 1차 프로젝트가 끝이나고 든 생각은 프로젝트는 나혼자만 잘 한다고 되는게 아니라는 것이었다.

프로젝트 전에는 대부분이 혼자서 작업하는 시간이었기 때문에 나만 잘하고 시간 맞춰 끝내면 됐지만 프로젝트는 팀으로 움직이기 때문에 나혼자 앞서나가는 것도 안되고 너무 뒤쳐져서도 안됐다. 그렇기 때문에 서로의 진행상황을 공유하고 문제가 있다면 함께 고민해서 헤쳐나가야지만 그 프로젝트를 성공 시킬 수 있다는 것을 다시 한번 깨달았다.

이번 프로젝트에서는 기술적인 부분에서도 많이 배울 수 있었다. 한달 전의 나와 비교하면 내가 이것까지 할 수 있다고?(예를 들어 장바구니 통신) 할 정도로 많은 성장을 이뤄냈다고 스스로 생각한다. 물론 아직 배워야 할 것은 태산이겠지만...ㅎㅎ 하지만 아직 나에게는 2차 프로젝트가 남아있고 성장할 기회가 아직 많이 있기 때문에 여기서 멈추지 않고 계속해서 달려나갈 예정이다

2차 프로젝트때는 1차에서 아쉬웠던 부분을 보충해나가면 좀 더 완성도 있는 결과물을 만들어내는 것이 목표인 것 같다. 1차에서는 백엔드와의 소통이 서툰점이 있어서 통신을 할 때 어려움이 있었는데(사전에 key값 맞춰놓기, 데이터를 어떤 형식으로 보낼지 혹은 받을지 등) 2차 때는 이런 점을 보완해보도록 해야겠다..!!

그럼 이제 다시 저는 열코딩하러 2000✋

1차 프로젝트 회고 끝✨
정육쩜 프로젝트 보러가기

profile
개발댕발

4개의 댓글

comment-user-thumbnail
2022년 10월 3일

정육쩜 넘 멋있습니닷😎🖤

1개의 답글
comment-user-thumbnail
2022년 10월 3일

수진님 수고하셨어용!!

답글 달기
comment-user-thumbnail
2022년 10월 4일

감성과 교훈이 공존하는 글이었습니다.

답글 달기