[회고록] 꾸꾸까까~? Kukka 클론 프로젝트 회고 (1)

*해당 클론 프로젝트의 경우 이미 있는 코드를 베낀 것이 아닌, DB 모델링 설계부터 중심 기능 구현까지 팀원들과 소통하며 직접 코드를 짠 프로젝트임을 밝힙니다.


1. Why Kukka?

꾸까를 선택한 이유는 처음 홈페이지에 접속했을 때 느낀 '깔끔하다'는 인상 때문이었다.
군더더기 없는 화면이 아직 개발을 배우는 입장에서 필요한 기능만 구현할 수 있겠다! 라는 생각이 들었다.

배너슬라이드, 카드 컴포넌트, 디테일페이지 계산 로직, 장바구니 기능이 있는 것을 보며
지금까지 배운 것을 적용해봄과 동시에 약간 높은 단계의 기능도 구현해볼 수 있다는 확신이 생겼다.
첫 프로젝트인만큼 욕심을 부리기 보다 배운 것을 리마인드하는 것에 초점을 두고자 했다.

2. 팀 컨벤션

예기치 못하게 팀원 한명이 빠지면서 4명으로 프로젝트를 시작하게 됐기에 더 똘똘 뭉치자는 마음이 생겼다.
그래서 데일리 미팅과 스프린트 미팅 만큼은 빠지지 않고 매일 진행하면서 서로의 진행 상황을 모두에게 공유하는 것을 1순위로 두었다.

정말이지 소통만큼은 다른 팀들이 인정할만큼 잘했다고 확신할 수 있다.
각종 클래스명, 변수명을 어떻게 표현할지 정하는 컨벤션도 물론 중요하지만, 해당 프로젝트를 통해 각자 어느 부분을 배우고 보완하고 싶은지, 진행 상황이 어떤 방식으로 공유되었으면 좋겠는지 등을 팀끼리 이야기 나누고 시작하면 훨씬 좋은 커뮤니케이션을 할 수 있게 된다.

3. 시연 영상





4. 기술 스택 / 기능 분담

기술 스택

프론트엔드 : React, Router, Sass
백엔드 : Node.js Express, My SQL, Prisma, Bycrypt, JWT
API Document : https://documenter.getpostman.com/view/15514335/Uyr4Jegx

역할 및 기능 분담

1차 프로젝트에 들어가기전, 백엔드에 더 관심이 많았던 나는 백엔드를 중점적으로 맡기를 원했다.
따라서 백엔드에서는 장바구니의 C.R.U.D API를 맡고,
프론트에서는 메인 페이지 전체를 만들기로 했으며 프론트로 구현한 대표 기능은 다음과 같다.

  • 배너와 메인 리스트 카드 슬라이드를 useRef와 useEffect를 이용하여 구현
  • 로그인시 Nav의 백엔드와의 통신을 통해 유저 이름과 장바구니 수량을 받아오는 기능
  • 스크롤 Y값에 따라 Nav를 handling



5. 새로운 시도와 깊어진 배움

1차 프로젝트를 진행하면서 새로 배우고, 느끼고, 경험한 것이 참 많다.
먼저 내가 배웠던 걸 새롭게 적용해본 코드와 몰랐던 개념을 새로 알게된 부분을 소개해 보려 한다.

5-1. Frontend [메인 페이지]

리스트 페이지에서 쓰이는 리스트카드 컴포넌트들을 묶어 메인 페이지에서 재활용한 코드다.
처음에는 겹치는 부분을 하드 코딩하여 짰는데 리액트를 배우는 입장에서 비효율적인 코드라 느껴졌다.

굳이 따로 컴포넌트로 빼야할만큼 많이 반복되는 코드는 아니었지만, 코드의 재활용성, 유지보수성을 높이는 것이 개발자의 과제이자 숙명이라 생각했기 때문에 재컴포넌트화를 진행했다.
무엇보다 props로 인자를 넘겨주는 로직에 대해 명확히 이해하지 못했던 만큼 더 스스로 해보고 싶었던 작업이었고, 다행히 기대했던 바대로 해당 과정을 더 잘 이해할 수 있게 됐다.

# Main.js
const [lists, setLists] = useState({ productList: [],});

//GET API로 받은 리스트 데이터 필터링 후 메인 리스트카드로 props 넘겨줌
const filtered_subscriptrion_list = lists.productList.filter(
    list => list.id > 9 && list.id <= 11);
const filtered_bouquet_list = lists.productList.filter(list => list.id <= 10);

  <MainListCard
       key={filtered_bouquet_list.id}
       lists={filtered_bouquet_list}
       bgColor="#fafafa"
       title="꽃이 필요한 순간,"
       subTitle="꾸까 꽃다발"
       text="더보기"
       linkTo={goToLink}
  />
            
# MainListCard.js
//리스트 페이지에서 쓰이는 리스트카드 컴포넌트 재활용   
function MainListCard({lists, bgColor, containerMargin, title, subTitle, text, linkTo, hide,}){
  return(
         <section className={styles.mainContainer}
          style={{backgroundColor: bgColor, margin: containerMargin}}>
           <div className={styles.flowerList} ref={cardRef}>
             {lists.map(list => (<ListCard key={list.id} list={list} />))}
           </div>
           <button
               type="button"
               className={styles.turn_list_card_btn}
               onClick={nextListCard}
               style={{ display: hide }}
               ></button>
         </section>
         );


5-2. Backend [장바구니 API]

프로젝트 하면서 가장 알고 싶었던 부분은 백엔드에서 만들어지는 API가 쓰이는 위치에 대한 것이었다.
그래서 장바구니 API는 C.R.U.D를 전부 만들어볼 수 있기 때문에 꼭 맡아서 성공하고 싶었던 기능!

단순히 머릿속으로만 로직을 생각하는 것이 어려워 엑셀로 적으니까 생각도 정리되고 이해도도 높아졌다.
이처럼 API를 만들때는 어떤 값을 프론트에서 요청해야 하고, 어떤 데이터를 백엔드에서 응답해줘야 하는지 명확히 하는 게 정말 중요하다.


Backend (1) : 미들웨어 토큰 검증 코드

우리 팀 장바구니는 회원만 이용할 수 있는 기능이라 관련 토큰 검증 절차가 꼭 필요했다.
코드를 짤 때 고민이 많았던 이유는 과연 매번 토큰검증 코드를 만들어야만 하는지에 의구심 들어서였다.

그 때 떠오른 개념이 백엔드 세션때 배운 미들웨어 함수였고, 이를 꼭 적용해보고 싶었다.
스택오버플로우와 jwt 깃허브를 참고하며 해당 미들웨어를 만들고, 포스트맨으로 POST API가 잘 작동하는 것을 봤을 때의 뿌듯함은 정말 이루말 할 수 없다.

//토큰 검증을 통해 유저 ID를 얻는 미들웨어
const jwt = require("jsonwebtoken");

const getUserIdByVerifyToken = async (req, res, next) => {
  const token = req.headers.token;

  if (token) {
    jwt.verify(token, process.env.SECRET_KEY, (err, decoded) => {
      if (err) {
        res.status(400).json({ message: err.message });
      } else {
        req.userId = decoded.id;
        next();
      }
    });
  } else {
    res.status(403).json({ message: "token is not provided" });
  }
};

module.exports = { getUserIdByVerifyToken };


Backend (2) : 장바구니 GET API의 CartDao 구조

프론트에서 배열로 받은 데이터를 풀어 DB 각 행에 비동기적으로 집어 넣는 코드다.
배열을 순차적으로 실행하는 메서드를 써야겠다는 것에 대한 감은 있었으나 어느 위치에 써야 되는지 모르겠어서 고민이 많았던 코드다.

여러 검색 끝에 prisma 공식 깃허브 중 Discussion에 올라온 코드에서 힌트를 얻을 수 있었는데,
결국 어떤 로직으로 짜야겠다는 약간의 감과 집요함이 문제 해결의 열쇠가 되는 것 같다.

//디테일 페이지에서 담은 상품을 DB의 product_carts 테이블에 담는 cartDao.js
const createUserCart = async (userId, productId, addOptionId, quantity, totalPrice) => {
  return addOptionId.forEach (
    async (optionId) =>
      await prisma.$queryRaw`
  INSERT INTO product_carts 
  (user_id, product_id, add_option_id, quantity, totalPrice, order_status) VALUES 
  (${userId}, ${productId}, ${optionId}, ${quantity}, ${totalPrice}, "주문 전")`
  );
};



6. 리팩토링이 필요한 코드

우리 꾸꾸까까 페이지의 Nav-bar다.
로그인 후 로컬 스토리지에 토큰이 들어왔을 때, 반갑습니다. {userName} 님!으로 되는 부분과 장바구니 수량이 들어오는 부분을 바로 리렌더링 시키는 것이 목표였다.

하지만, state로 관리되어야할 토큰을 const로 선언하는 바람에 리렌더링이 일어나지 않는 문제가 있었다.
이 부분을 리팩토링 기간에 반드시 고치고 싶었으나 팀 내에서 우선적으로 해결해야할 문제가 생겨 아직 못고쳤다.. context API를 써서 고치고 있는 중인데 해결되는대로 리팩토링 후의 코드도 올려야지!

// 리팩토리전 Nav 코드
  const token = localStorage.getItem('token');
  const [counter, setCounter] = useState(0);
  const [isLogIn, setIsLogIn] = useState(false);

  useEffect(() => {
    fetch('/carts', {
      headers: {
        'Content-Type': 'application/json',
        token: token,
      },
    })
      .then(res => res.json())
      .then(data => {
        setCounter(data.userCart.length);
      });
  }, [isLogIn]);

  useEffect(() => {
    if (token) {
      setIsLogIn(true);
    } else {
      setIsLogIn(false);
    }
  }, []);



7. 귀담아 듣는다는 건

프로젝트를 하면서 예상치 못한 문제에 부딪힐 때마다 느낀 것은 완전히 새로운 문제는 별로 없다는 것이었다.
저번에 겪었던 문제거나, 누군가 이미 겪어본 문제라 약간의 응용력으로 해결할 수 있는 문제가 많다.
따라서 같은 과정을 겪어본 사람으로부터 조언을 귀담아 듣는 것은 개발자에게 필요한 덕목 중 하나가 된다.

7-1. 예상 작업 기간 * 10

프로젝트를 시작할 때, 멘토님께서 모두에게 해주신 말씀이 있었다.
바로 예상 작업기간에 0을 더 붙히라는 것이었다.
이 말씀은 우리가 기능에 욕심을 부릴 때마다 서로를 컨트롤하는 마법의 단어가 되어 주었다.

그래서 우리는 리스트 레이아웃이 완료되고, 그 데이터를 불러오는 API가 완성되자마자 일찍 프론트-백엔드를 붙혀보았다.
겨우 프로젝트 4일차였다. 함께 회고하면서 가장 잘한 선택이었다고 느낄 만큼 좋았던 결정이자 조언을 잘 흡수한 예다.**

7-2. 데이터 구조의 중요성

백엔드에서 프론트로 데이터를 넘겨줄 때, 프론트 입장을 고려하여 배열에 담아 준 적이 있다. 헌데 배열 3개를 map을 돌린 후 자식 컴포넌트에 props로 전달하는 과정에서 map이 불필요하게 많이 사용되었다.

이 부분에 대해 도움을 요청하고 받은 조언으로부터 그동안 데이터구조를 깊게 고려하지 않은 쿼리문을 작성했음을 깨닫게 되었다.
객체와 배열이 각각 담는 데이터의 차이를 고려하지 않았다니..!
배열은 비슷한 속성을 묶는 것이고, 객체가 한 대상을 나타내는 서로 다른 속성을 포함한다는 걸 다시 한 번 상기시키게 된 경험이었다.

8. 시행착오

지옥의 Git Revert

다음은 눈물없이 볼 수 없는 git revert 로그이다.
사건발생 경위는 우리 백 레포지토리의 main이 막혀있지 않은 상황에서 내가 실수로 메인으로 PR을 날렸고,
그 누구도 눈치채지 못한채 PR승인후 merge 시키면서 시작됐다.

다행히 main을 이전 상태로 리버트 시키는 방법을 알게됐는데, 한 팀원이 PR 대상을 develop으로 변경해야 하지 않냐고 의견을 냈다.
리버트 명령어의 원리에 대해 제대로 인지하지 못한채 main이 아닌 develop에 잘못 리버트를 시켜버렸고, 그렇게 우리 먼 강을 건너게 되었다.

1시간동안 revert와 싸움하며 깨닫게 된 것은,revert를 진행하면 이전 커밋 내용이 담긴 revert용 branch가 생기게 되고 그것을 PR 대상에 덮어 씌운다는 것이다.

정말 git 명령어는 잘 모르는 채 함부로 쓰는거 아니라는 큰 교훈을 얻게 됐다..끌끌
다시 한 번 느끼지만, 깃헙을 통해 협업할 때는 경로 확인을 똑바로 하고 어디에 머지되어야 하는지를 계속 생각하면서 해야한다!
이렇게 문제 해결 후 해결 방법만 제대로 이해한다면 그 경험의 가치는 빛을 발할 수 있다. 깃 리버트,, 힘들었지만 너무 가치있던 경험.

첫 프로젝트여서 그런지 정말 회고할 내용이 많아졌다.
따라서 나의 좀 더 딥한 이야기를 담은 회고록은 2탄에서 진행해보고자 한다:)

좋은 웹페이지 즐겨찾기