Converse Website Clone Project 후기

손병진·2020년 9월 26일
2

wecode

목록 보기
23/27

결과물📽

해당 프로젝트는 프론트엔드 4명 백엔드 2명으로 이루어진 팀으로 진행했으며,
각자 로그인, 회원가입 / 메인 / 제품목록 / 제품상세 / 장바구니 / 위시리스트 페이지로 역할을 분배하였다. 필자는 제품목록과 위시리스트는 담당했다.

제품목록

위시리스트


기억하고 싶은 코드💯

무한 스크롤

컨버스 제품 목록은 1,2,3 페이지로 나누는 pagination 방식이 아닌 스크롤를 내려 일정 위치에 도달하면 제품을 추가적으로 보여주는 무한 스크롤 구조였다.

스크롤 속성값

  • scrollHeight : 전체 페이지의 높이
  • clientHeight : 현재 사용자가 보고있는 화면의 높이
  • scrollTop : 스크롤을 내려 지나간 페이지의 높이
  • 현재페이지가 전체페이지 맨 아래에 다다랐을 때
    = 현재페이지의 높이와 지나간페이지의 높이의 합이 전체페이지 높이일 때
  infiniteScroll = (e) => {
    // documentElement, body 속성의 scroll 값이 다를 수 있다고 하였고
    // 다를시 둘 중에 최대값을 사용하는 것이 안전하다는 포스팅을 참고하였음.
    let scrollHeight = Math.max(
      document.documentElement.scrollHeight,
      document.body.scrollHeight
    );
    let scrollTop = Math.max(
      document.documentElement.scrollTop,
      document.body.scrollTop
    );
    let clientHeight = document.documentElement.clientHeight;

    if (scrollTop + clientHeight === scrollHeight) {
      this.pagination(); // 데이터 호출 함수
    }
  };

참고사이트
페이지의 기하학 속성 값
React 무한스크롤 구현하기

두가지 방식

  • fetch(백엔드 중심)
    처음에는 다른 제품 사이트와 같이 해당 페이지 번호를 눌렀을 때, 일정량의 데이터를 새로 받아오는 방식이라고 생각하였고, 위 조건문이 맞았을때 fetch 함수를 실행시키고자 하였다.
  • state(프론트엔드 중심)
    그런데, 컨버스 공식 사이트를 탐색하는 도중에 의문점이 들었다.
    제품 목록 페이지에서 배너와 이미지 사이에 수평 바를 보면 상품의 총 수량이 나와있는 것을 알 수 있다. 그리고 총 수량은 카테고리를 바꾸거나 필터링을 할때는 바뀌지만 스크롤 이벤트일 때는 변하지 않았다.

"처음부터 데이터 전체를 받아오는 게 아닌데 어떻게 총 수량을 카운트 하는거지?"

라는 의문이 생겼고, 개발자도구를 확인해보니
백엔드에서 그때 그때 데이터를 불러오는 것이 아닌 처음부터 모든 제품 데이터를 가져와 프론트엔드에서 직접 필터링한 내용을 기반으로 UI를 구현했다는 것을 알 수 있었다.

  pagination = () => {
    // 먼저 전체 데이터와 보여줄 데이터를 나누어 관리하였다.
    const { wholeProducts, products } = this.state;
    
    // 보여줄 제품이 없을 때는 멈추게끔 조건문 설정
    if (wholeProducts.length > products.length) {
      
      // 로딩 이미지 관련 함수
      this.setState(
        {
          loadingStatus: !this.state.loadingStatus,
        },
        
        // 전체 데이터에서 14개씩 호출
        () => {
          let result = wholeProducts.slice(
            products.length,
            products.length + 14
          );

          // 로딩이미지를 일정 시간 보여줄 수 있게끔 setTimeout
          setTimeout(
            () =>
              this.setState(
                {
                  loadingStatus: !this.state.loadingStatus,
                },
                
                // 호출한 데이터를 보여줄 데이터로 병합
                () => {
                  this.setState({
                    products: [...this.state.products, ...result],
                  });
                }
              ),
            1000
          );
        }
      );
    }
  };

setState + props (feat.유레카)

  • 이번 프로젝트 최고의 난제
    제품 목록에서 이미지 하단에 색상에는 두가지 이벤트가 있다.
  1. 호버
    해당 색상의 이미지가 전환되어 보이게끔 css 설정
  2. 클릭
    해당 이미지가 메인으로 고정되어 바뀌게끔 설정되어있다.
    일단, 데이터를 공식 사이트에서 크롤링하는데 메인이미지와 색상별 이미지를 따로 만들어 보내주었다.
    (왜냐하면, 색상별 이미지만 받아서 구현하면 같은 이름의 제품이 두번 이상 페이지에 걸려있을때, 동일하게 나올 수 있기 때문에 메인 이미지를 별도로 나누었다. 그리고 여담이지만, 공식 사이트에서 대체 왜 동일한 이름의 제품을 두번 이상 목록에 배치해놓았는지 이해하기 어려웠다.)

프론트엔드에서도 데이터 가공이 가능하다

호버 이벤트에서는 토글 개념과 z-index 활용하여 함수를 만들었는데, 여기에 토글을 이중으로 추가할 수는 없었다. 그래서, 주어진 데이터를 그대로 사용하여 속성을 바꾸어 주는 것이 아닌, 받은 데이터 중에서 메인 이미지를 따로 저장한 뒤에 이벤트가 일어났을 때, 그 데이터 자체를 바꿔주는 원리를 생각했다.
처음에는 js 파일을 만들어 import 하려 했지만 나중에는 state 값을 활용했다

setState + render

state 활용했던 이유는 lifeCycle 때문이었다.
처음에 js 파일로 만들고 데이터를 수정할 때에는 아무 일도 일어나지 않았다. 데이터만 바뀌고 UI 에 전혀 반영되지 않는 것이다. 그리고 알았던게 setState 이루어지면 렌더링이 다시 된다는 사실이다.
그래서 state 값에 메인 이미지 Url 값을 수정했지만.. 아무일도 일어나지 않았다.

props + 함수의 위치 + render

활용하는 이벤트와 값이 자식 컴포넌트에 있기 때문에 데이터 또한 props 이용하여 가져가서 변경했더니 아무것도 일어나지 않았다.
여기서 깨달았던 것은 setState 함수가 일어나는 컴포넌트부터!!! render 가 일어난다는 것이다.
즉, 자식 컴포넌트에서 setState 일어나도 부모 컴포넌트에는 render 일어나지 않아 아무 반응이 없는 것이다.
그래서 데이터를 props로 가지고 내려가는 것이 아닌!!
해당 이벤트와 값을 자식에서 부모로 가지고 올라가서 setState 함수를 실행시켜주어야 바뀐 데이터가 UI에 반영될 수 있었다.

여기서 중요한 점은 부모-자식 관계를 이해하여 이벤트가 일어났을때 render가 되어야 하는 지점을 정확하게 파악하고 있어야 한다는 점이다.

해당 원리를 쉽게 풀 수 있도록 추후에 redux 개념을 학습할 예정이다.

  • 유레카
    지금 이렇게 쉽게 풀어쓰지만 그날 하루종일 고민하다가 마지막에 풀었을 때 이성을 잃고 환호를 지르며 뛰쳐나갔었던 굉장히 민망한 기억이 있다.

토글(중복 선택)

제품 목록 페이지에서 좌측 수평 필터링 레이아웃에 숨기기 기능이 있다.
각각 별개로 작동하지만 동일한 기능이기에 간략하게 코드 구조를 고민하였다.

Active Tab, Menu Tab 과의 차이

기본적인 토글 구조를 기반으로 하는데 메뉴탭과 다른점은 서로를 교체, 대체하는 것이 아닌 각각의 기능이 별개로 작동한다는 점이다. 그래서 각각 고유 값을 부여해주어야 한다. 그래서 state 값에 배열을 넣고, 필터 메뉴의 개수 만큼 Boolean 값을 넣었다. 그리고, 각 메뉴별로 자신에게 해당하는 값을 바라보게 하여 className을 변경해주었다.

slice

  // 메뉴에 해당하는 인덱스 값을 가져와 이에 대응하는 위치의 state 값을 바꿔주는 함수
  hideFilter = (e) => {
    this.setState({
      hideFilterImage: [
        ...this.state.hideFilterImage.slice(0, e),
        !this.state.hideFilterImage[e],
        ...this.state.hideFilterImage.slice(e + 1),
      ],
    });
  };

this.props.children

  • 같은 css 구조에 다른 내용이 담겨있고, 반복되어 나타날때 사용할 수 있는 개념이라고 한다.
    피드백을 받았지만 반영하지 못해, 다음에는 꼭 활용해보고 싶다.
    React.children 에 대하여

post 통신 + header(token)

  • 제품 목록에 하트를 클릭했을 때 위시리스트에 담길 수 있게끔 post 통신을 했다.
  • GET, POST 모두 header : token 이 있어야 한다
  // 위시리스트 보내는 함수
  handleHeart = (mainId) => {
    fetch(`${secondAPI}/account/wishlist`, {
      method: "POST",
      headers: {
        Authorization: localStorage.getItem("token"),
      },
      body: JSON.stringify({
        id: mainId,
      }),
    })
  };

  // 위시리스트 받는 함수
  getWishList = () => {
    fetch(
      `${API}/account/wishlist`,
      {
        headers: {
          Authorization: localStorage.getItem("token"),
        },
      }
    )
      .then((res) => res.json())
      .then((res) => {
        this.setState({
          wishlist: res.wishlist,
        });
      });
  };

location(동적 라우팅)

  • 메인에서 제품목록 페이지로 넘어갈 때 location 개념을 활용했다.
    사실, location 이나 match 모두 url 주소를 활용한다는 개념이고 용도 명확히 나뉘지 않는다는 느낌을 받았다.
  • 주의
    location 개념을 활용할 때에는 Route.js 주소에 url/:id 이런식으로 뒤에 붙여서는 안된다.
    이런 식으로 해놓으면 this.props.location 값이 인식되지 않는다. 이것은 match.params 활용할 때에 설정하는 주소이다.
 getDataInitial = () => {
   
    // url 주소 중에서 활용할 문자열을 가져오는 식이다.
    const categoryId = this.props.location.search.split("=")[1];
   
   		// 이 문자열을 활용하여 쿼리스트링을 구성한다.
        fetch(
          fetch(`${firstAPI}/products?sub_category_id=${categoryId}`
          {
            headers: {
              Authorization: localStorage.getItem("token"),
            },
          }
        )
          .then((res) => res.json())
          .then((res) => {
            let result = res.products.slice(
              this.state.products.length,
              this.state.products.length + 14
            );
        });
    );
  };

짧게 느낀 점을 애기하자면
개인적으로 굉장히 재밌었다. 하하호호 거리며 진행했던 것은 아니지만, 이렇게 치열하게 밤늦게까지 어떤 작업을 했던 기억이 정말 오랜만이었다. 앞으로 이런식으로 살아야겠다는 생긱이 든다. 또 다음 프로젝트가 있겠지만 이번 기간동안 함께 해주신 그리고 모든 동료들에게 감사하다는 말씀을 드리고 싶다.

항상 팀원들과 프로젝트 진행 상황을 확인하고 챙겨주신 PM 상호님
시각적으로 화려한 부분아 아니어서 답답하셨을텐데 묵묵히 자기 역할을 가장 완벽하고 성실하게 수행해주신 호균님
일과 병행하고 장비가 망가지는 해프닝이 있어도 지치는 기색 없이 프로젝트에 열의를 보이며 함께해주셨던 에이스 영빈님
너무나도 든든하게 저희가 받쳐드리지 못할 정도의 역량을 보여주신 두 백엔드 팀원 경훈님과 수정님
이 분들과 함께 했어서 행복할 수 있었던 것 같습니다. 함께 해주셔서 감사하고 영광이었습니다!

profile
https://castie.tistory.com

4개의 댓글

comment-user-thumbnail
2020년 9월 27일

유레카 짤ㅋㅋㅋ딱 병진님이네요,,

1개의 답글
comment-user-thumbnail
2020년 9월 28일

이열

답글 달기
comment-user-thumbnail
2020년 9월 28일

잘보고갑니다

답글 달기