Wecode 1차 프로젝트 - Dr.Jart+ 클론 코딩 후기

Shin Yeongjae·2020년 8월 2일
2

Wecode

목록 보기
20/26

프로젝트 소개🙌

  • 에스티로더가 아시아 뷰티 브랜드를 인수한 첫 사례의 대표적인 국내 화장품 브랜드 Dr.Jart+ 클론코딩

개발 기간📆

  • 2020년 7월 20일 ~ 2020년 7월 31일(2주)

팀원🐙


개발 목적🧾📈

  • Dr.Jart+ 홈페이지를 클론코딩하며 사용자 경험을 고려하여 UI의 중요성을 이해하고 레이아웃 배치를 익힌다.
  • React를 다루는데 꼭 필요한 객체(Object)의 정의, 객체의 속성 접근 및 조작 방법을 이해한다.
  • Class형 컴포넌트를 다루며 React의 필수 개념인 LifeCycle, State, Props를 좀 더 직관적으로 다루고 이해해본다.

기술 스택 및 구현 기능🛠

기술 스택

  • React.js
  • React Router
  • RESTful API
  • Sass
  • Slick.js
  • Intersection Observer API
  • AWS EC2

구현 기능

다음은 사이트 주요 기능이다.
직접 구현한 기능은 ✅, 팀원이 구현한 기능은 ✔️로 표시했다.

  1. 📃 회원가입 / 로그인 페이지
    1-1. ✔️ 이메일 및 패스워드 양식 확인 기능
    1-2. ✅ 로그인 기능 및 카카오, 구글 소셜 로그인 기능(localStorage)

  2. 📃 메인 페이지 / 이벤트용 메인 페이지
    2-1. ✔️ 네비게이션 바 호버 기능
    2-2. ✔️ 사이드 네비게이션 바 클릭 시 펼쳐지는 기능
    2-3. ✔️ 메인 페이지 Carousel 기능(Slick.js, CSS)
    2-4. ✅ 메인 페이지 상품 컴포넌트 구성
    2-5. ✅ 메인 페이지 하단 리뷰란 제목 일정 글자 이상 넘어가면 ...으로 줄여주는 기능

  3. 📃 상품 목록 페이지 / 상품 상세 페이지
    3-1. ✅ 상품 목록 페이지 Image Lazy Loading 기능
    3-2. ✅ 상품 목록 정렬 기능
    3-3. ✅ 상세 페이지 동적 라우팅
    3-4. ✅ 상품 목록 hover시 바로구매 버튼 및 장바구니 추가 버튼 나타남
    3-5. ✅ 장바구니 버튼 클릭 시 장바구니에 목록 추가
    3-6. ✅ Scroll 이벤트에 throttle 적용(lodash.js 미사용)
    3-7. ✅ Scroll 위치에 따라 나타나는 floating nav
    3-8. ✅✔️ Scroll 위치에 따라 생기는 애니메이션
    3-9. ✅ 상세 페이지 사이드바 수량 및 금액 state 공유
    3-10. ✔️ 상세 페이지 Carousel 기능(Slick.js)
    3-11. ✔️ 제품 상세정보 html 삽입(dangerousSetInnerHTML)

  4. 📃 장바구니 페이지
    4-1. ✅ 가격 및 수량 카운트, 삭제 기능
    4-2. ✔️ 장바구니 목록 레이아웃

💡 담당한 기능 중 특히 신경 쓴 부분 :

  • 메인 페이지: 상품 컴포넌트 구성
  • 상품 목록 페이지: 백엔드에서 수신한 데이터에 맞게 조건부 렌더링되는 컴포넌트
  • 상세 페이지: 사이드바
  • 장바구니: 물품 추가, 가격 및 수량 카운트, 삭제 기능

파일 트리 구조

.
├── Components
│   ├── Footer
│   │   ├── Footer.js
│   │   └── Footer.scss
│   └── Nav
│       ├── Nav.js
│       ├── Nav.scss
│       └── SideNav
│           ├── SideNav.js
│           └── SideNav.scss
├── Config.js
├── Pages
│   ├── Cart
│   │   ├── Cart.js
│   │   ├── Cart.scss
│   │   ├── Cartlist.js
│   │   └── Cartlist.scss
│   ├── Detail
│   │   ├── Detail.js
│   │   ├── Detail.scss
│   │   ├── DetailNav.js
│   │   ├── DetailNav.scss
│   │   ├── FloatingNav.js
│   │   └── FloatingNav.scss
│   ├── Main
│   │   ├── Main.js
│   │   ├── Main.scss
│   │   ├── MainStory
│   │   │   ├── MainStory.js
│   │   │   └── MainStory.scss
│   │   └── Review
│   │       ├── Review.js
│   │       └── Review.scss
│   ├── Products
│   │   └── Product
│   │       ├── Each.js
│   │       ├── Each.scss
│   │       ├── Product.js
│   │       └── Product.scss
│   ├── SignIn
│   │   ├── SignIn.js
│   │   └── SignIn.scss
│   └── SignUp
│       ├── SignUp.js
│       └── SignUp.scss
├── Routes.js
├── Styles
│   ├── common.scss
│   └── reset.scss
├── Util
│   └── throttle.js
└── index.js

16 directories, 36 files

기록하고 싶은 코드

개인적으로 아쉬움이 많이 남기도 하고 나름 잘 짠것 같다는 코드들이다.
(아직 리팩토링을 하기전이라 좀 부끄럽다)

  • 상세 페이지 Scroll 이벤트 throttle 적용
    lodash.js에서 제공하는 throttle 함수를 사용하지 않고 직접 구현했다.
    throttle 함수는 다음과 같다.
const throttle = function (func, delay) {
  let timer;
  return function () {
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        func(...arguments);
      }, delay);
    }
  };
};

export default throttle;

인자를 함수와 딜레이를 주고 실행될때마다 throttle이 걸리게 했다.
Scroll 이벤트는 다음과 같다.

  componentDidMount() {
    fetch(`${API_URL}/product/detail/${this.props.match.params.id}`, {
      method: "GET",
    })
      .then((res) => res.json())
      .then((result) => {
        this.setState({
          preview: result.images,
          review: result.reviews,
          html: result.datas,
        });
      });
    window.addEventListener("scroll", throttle(this.handleScroll, 100));
  }

  handleScroll = (e) => {
    const scrollTop = ("scroll", e.srcElement.scrollingElement.scrollTop);
    this.setState({ scrollTop });
  };

componentDidMount에 함수를 걸어 Scroll 이벤트가 바로 실행되게 하였다.
그리고 props로 상세 페이지 네비게이션바에 state를 넘겨주었다.

...
<div
  className="detailInfo"
  dangerouslySetInnerHTML={
    html.length
      ? { __html: html[0].product_detail__detail_html }
      : null
   }
  ></div>
</div>
<DetailNav scrollTop={scrollTop} />
...
{scrollTop > 650 ? (
          <FloatingNav
            title={name}
            price={product_detail__price}
            salePrice={product_detail__price_sale}
            count={count}
            handleCount={this.handleCount}
          />
        ) : (
          ""
        )}

scrollTop 수치에 따라 FloatingNav가 나타나고 사라지게하였다.
그리고 가격과 수량도 props로 넘겨줘서 같은 내용이 나타나게 했다.

개인적으로 가장 신경을 많이 쓴 코드라고 생각하는데 그렇게 막 깔끔하게 짠 것 같지는 않아서 아쉬움도 많이 남는다. 리팩토링하면서 수정/보완할 생각이다.

  • 상품 목록 페이지 리스트 정렬 기능
sortDatas = (e) => {
    e.stopPropagation();
    const { productDatas } = this.state;
    const { value } = e.target;
    const priceObj = {
      "낮은 금액 순": "product_detail__price",
      "높은 금액 순": "product_detail__price",
      "인기 순": "star_average",
      "신상품 순": "created",
    };
  
    let newData = productDatas.sort((a, b) => {
      if (value === "낮은 금액 순") {
        return a[priceObj[value]] - b[priceObj[value]];
      } else if (value === "신상품 순") {
        return new Date(b.created).getTime() - new Date(a.created).getTime();
      }
      return b[priceObj[value]] - a[priceObj[value]];
    });

    this.setState({
      isClicked: false,
      curCategoryValue: value,
      productDatas: newData,
    });
  };
{categories.map((el) => {
   return (
     <li key={el}>
       <label>
         <input
           type="radio"
           value={el}
           onClick={(e) => this.sortDatas(e)}
          />
          {el}
       </label>
     </li>
   );
})}


이렇게 구성된 카테고리 목록 각 항목에 value에 해당하는 정렬을 하여 목록을 보여줘야 했다. 처음에는 어떻게 해야 할지 감이 잘 안잡혔는데, 동기 한 분이 맵핑을 해서 조건에 맞게 짜보면 어떨까 하고 힌트를 주셔서 금방할 수 있었다.
switch문으로도 작성할 수 있었지만 개인적으로는 switch문을 선호하지 않아서 if else 문으로 처리를 했다. switch문은 어딘가 모르게 백엔드의 향기가 난다.

사실 프로젝트 들어가기 전 풀어본 repl.it에 있는 내용이었는데 까먹고 있었다. 복습의 중요성을 다시 한번 느낀 시간이었다.

  • 장바구니 기능 관련 코드
    장바구니가 개인적으로 가장 아쉽다. 금요일이 프로젝트 종료일이었는데, 수요일 저녁쯤에 장바구니를 하자는 이야기가 나와서 목요일 저녁에 끝을냈다. 정말 초인적인 힘으로 완성한 것 같다. 하지만 개발 기간이 짧아서 그렇게 완성도있는 장바구니를 만들지 못했다.
componentDidMount() {
    fetch(`${API_URL}/user/order`, {
      method: "POST",
      headers: {
        Authorization: localStorage.getItem("Kakao_token"),
      },
    })
      .then((res) => res.json())
      .then((res) =>
        this.setState({
          cartlist: res.data,
        })
      );
  }

  componentDidUpdate(prevProps, prevState) {
    if (prevState.reload !== this.state.reload) {
      window.location.reload();
    }
  }

  getAllprice = () => {
    const { cartlist } = this.state;
    let sum = 0;
    cartlist.forEach((el) => {
      sum += el.price;
    });
    return sum;
  };

  getAllsalePrice = () => {
    const { cartlist } = this.state;
    let priceSum = 0;
    let salepriceSum = 0;

    cartlist.forEach((el) => {
      if (el.price_sale) {
        priceSum += el.price;
        salepriceSum += el.price_sale;
      }
    });
    return priceSum - salepriceSum;
  };

  setReload = () => {
    const { reload } = this.state;
    this.setState({
      reload: !reload,
    });
  };

상품 목록 페이지에서 장바구니 버튼을 클릭하면 백엔드 서버로 상품 id를 보내는데 그걸 받아놨다가 장바구니 페이지에 들어가자 마자 fetch를 실행하여 상품 정보들을 받아오게 구성했다. 그러나 급하게 코드를 짜다보니 구조를 잘못 짜서 정보가 바뀔 때 새로고침을 하지 않으면 바뀌지 않아서 강제로 reload를 하게 만들었다. 급하게 마무리 지어야 했기 때문에 어쩔 수 없지만 리팩토링하면서 아예 새로 구조를 짜볼 생각이다.

handleCount = (productNum, sign) => {
    fetch(`${API_URL}/user/order${sign === "+" ? "add" : "minus"}`, {
      method: "POST",
      body: JSON.stringify({ product_id: productNum }),
      headers: {
        Authorization: localStorage.getItem("Kakao_token"),
      },
    })
      .then((res) => res.json())
      .then((res) => {
        this.setState({
          count: res.quantity,
          price: res.price,
          salePrice: res.price_sale,
        });
        this.props.setReload();
      });
  };

  deleteItem = (productNum) => {
    fetch(`${API_URL}/user/orderdelete`, {
      method: "POST",
      body: JSON.stringify({ product_id: productNum }),
      headers: {
        Authorization: localStorage.getItem("Kakao_token"),
      },
    }).then((_) => window.location.reload());
  };

그리고 장바구니 목록을 렌더하는 컴포넌트에서 각 버튼을 클릭할 때 마다 fetch 요청을 보내서 백엔드 db에 반영되게 하였다. 이 과정도 강제로 새로고침이 되게한거라 너무 아쉽다. count 관련 함수도 원래는 plus 함수와 minus함수가 각각 나눠져있었는데 멘토 종택님이 api 주소만 다르고 다른 내용은 같으니 삼항연산자를 활용하여 하나로 줄이는게 어떻겠냐고 아이디어를 주셔서 코드량을 많이 줄일 수 있었다. 그리고 reload 관련 함수는 멘토 승현님이 아이디어를 주셨다. 승현님 최고👍


느낀점

잠깐 공부한 리액트로 작업하다보니 레이아웃을 짜는데 상당한 시간이 들어갔다. 어떻게 해야 효율적으로 만들지 엄청 고민했다. 퍼블리셔분들은 참 대단한 분들이라고 느꼈다. 그리고 백엔드랑 소통하는 것이 가장 어렵다고 느꼈다. 서로 용어를 몰라서 설명하는데 어려움을 겪었고 무엇보다 백엔드 API가 나오는 속도를 따라가지 못해 급하게 작업을 한 감이 없지 않아 있다. 그리고 Trello를 처음 사용해봤는데 처음 써보는거라 활용을 제대로 못한 것 같아 아쉽다. 프로젝트 중간에 팀원 한 명이 나가면서 2명이서 모든 페이지를 만들었는데 너무나 힘들었다. 기간은 한정적인데 많은 작업을 하다보니 제대로 신경을 못쓴 것 같아 아쉬웠다. 그래서 나는 기능쪽을 담당하고 다른 팀원은 css 부분을 도맡아서 했다. 2차때는 css 부분에 좀 더 집중해야겠다는 생각이 들었다.

내 욕심때문에 팀원에게 너무 큰 부담을 안겨준 것이 아닌가 하는 생각도 들었다. 여러가지 기능을 넣어보려고 하다보니 하나도 제대로 만든 페이지가 없다고 느껴졌다. 다들 잘했다고 칭찬했지만 너무나 아쉬움이 남는다.

다음 프로젝트때는 기획 단계에 시간을 많이 투자해서 버리는 시간 없이 효율적으로 작업을 진행해야겠다. 그리고 새로운 기술에 집착하기 보단 기본에 충실하여 한 페이지라도 제대로 작업해야겠다.

profile
문과생의 개발자 도전기

1개의 댓글

comment-user-thumbnail
2020년 8월 3일

💪💪💪💪

답글 달기