프리온보딩 | 3번째 과제 회고 - 1주차

noopy·2022년 2월 5일
4

프리온보딩

목록 보기
3/6

Wanted-preonboarding 1-3

깃허브 링크
배포 링크

📕 구현 명세

사진과 가구 정보를 조합하는 컴포넌트 구현하기

필수 구현 사항

  • 가구 정보가 있는 곳에 돋보기 모양의 버튼을 표시 ✅
    • 돋보기를 클릭하면 상품정보 tool tip 출력되면서 돋보기모양이 닫기 버튼으로 변경 ✅
    • 닫기 버튼을 클릭하면 tool tip을 없애고 돋보기 버튼으로 변경 ✅
      tool tip은 하나만 노출 ✅
    • tool tip이 노출되고 있는 상태에서 다른 가구를 선택하면 노출되고 있던 tool tip은 닫히고 새로 클릭한 가구 tooltip만 노출 ✅
    • 하단에 있는 상품목록에서 해당 가구가 선택되었으면 tooltip 출력 ✅
  • 할인율이 존재하는 경우 상단에 할인율 표시 ✅
  • 입점된 가구, 입점되지 않은 가구 다르게 표시 ✅

추가 구현 사항

  • 데이터 저장
    local storage에 저장된 데이터가 없을 경우에만 API 호출, 있을 경우 재사용하여 새로고침시에도 데이터가 날아가지 않게 구현
  • Tooltip 위치 표시
    이미지의 width, height 절반씩 보다 pointX, pointY가 클 경우 말풍선과 tip의 위치가 변경
  • 하단 슬라이드 구현
    두 번이상 반복된 스타일들은 @styles/commonStyles에 재활용 가능하도록 저장

🪞 데모

필수 구현 사항

추가 구현 사항

⏰ 진행 과정

이번에 처음으로 타입스크립트와 리덕스를 적용해 과제를 진행했다.

typescript

기존에 JS에서도 타입을 보완하기 위해 propTypesdefaultProps를 항상 넣어주긴 했지만, 컴포넌트 외에도 함수나 다른 모듈에서도 타입을 명확히 할 필요가 있었다.

여태까지 계속 새로운 스킬을 배우면서 바로 프로젝트에 진행해야 했기 때문에 상대적으로 러닝커브가 높은 타입스크립트는 계속 뒷전으로 미루게 되더라. 이번엔 개인과제 이기도 하고 이제 리액트는 조금 알 것도 같아서 다른 분들의 프로젝트 결과물과 구글링을 통해 열심히 적용했다....! 특히나 잘하는 분들의 프로젝트 코드를 보는 건 정말 많은 도움이 되었다.

redux

이번과제에서 리덕스를 적용할 필요는 없다고 느껴졌지만, 다음 주부터 본격적으로 리덕스를 사용하라는 과제가 나올 것 같아서 미리 경험해볼 생각이었다. 사실 쉽지 않았다 😱. redux, redux-thunk, redux-saga, typesafe-actions, recoil, mobx... 등 종류가 너무 다양해서 당장 뭐부터 배워야 하는지 헷갈렸다. 리덕스의 기본 개념을 잘 알지못해서 생기는 문제로 간단히 살펴보았다.
요약하면 리덕스는 스토어를 사용해 컴포넌트 외부에 상태를 두고 상태를 업데이트하거나 전달받을 수 있는 도구이다. 미들웨어는 dispatch(action 발생)된 액션을 스토어에 넘기기 전에 어떤 작업을 처리하고 싶을 때 사용한다. 리덕스가 동기적인 흐름이기 때문에 비동기적인 작업을 처리하고 싶을 때 미들웨어를 사용한다. 미들웨어는 대표적으로 redux-thunk, redux-saga가 있는데 contextAPI와 문법이 비슷한 redux-thunk를 사용해보기로 했다.
리덕스를 사용하기 위해 셋업해야될 게 많고 복잡해서 이 프로젝트에 적용하기엔 투머치였다고 느꼈고, context API를 직전에 공부한 덕분에 쉽게 이해할 수 있었다. 폴더도 너무 많고 복잡하기 때문에 다음엔 좀 더 직관적인 mobx를 사용해볼 것이다.

🔑 KEY POINT

  1. 로컬 스토리지 값이 없는 경우만 API를 호출해 재사용성 높이기
    사용할 데이터 값이 같고 뭐 할 때마다 API를 요청할 필요가 없기 때문에 로컬스토리지를 사용해 데이터를 재사용하였다. useLocalStorage 훅과 useGetProductList 훅을 조합해 로직을 작성했다.

  1. 로직이 두번씩 호출되는 이유 알아내기
    돋보기 모양 버튼을 누를 때마다 useClickAway 훅이 두번씩 선언되는 버그를 발견했다. dispatch가 두번씩 호출되기 때문에 정말 큰 버그였다. 디바운스도 써보고, useRef에서 훅이 선언될 때마다 더해서 2가 되면 로직이 작성되게 하거나 온갖 방법을 다 써봤는데 안돼서 어떻게든 돌아가게는 만들었지만 너무나 해괴한 코드가 탄생했다.
   useEffect(() => {
     const listener = (event) => {

       const target = event.target.closest('.toggle');
       const clickedId = target ? +target.dataset.id : 0;
       ++countClickRef.current;
       if (saveClickedId.current === clickedId && countClickRef.current === 2) {
         dispatch(updateActivedId(0));
         // 0을 보내면 tooltip이 닫힘
       }
       if (saveClickedId.current !== clickedId || countClickRef.current === 1) {
         dispatch(updateActivedId(clickedId));
       }
       if (countClickRef.current === 2) {
         countClickRef.current = 0;
       }
       saveClickedId.current = clickedId;
       dispatch(updateActivedId(clickedId));
     };
 	// ... 이외 생략

(마치 이런 느낌....)

그러다 전체 컴포넌트를 가운데 정렬하고자 위치를 옮겼는데 그자리에 돋보기 버튼들이 그대로 있었다(?) 😱. Hㅏ....중복으로 컴포넌트를 넣어버린 것이다. document에 적용된 클릭 이벤트가 두 컴포넌트에 들어가면서 두번씩 호출된 걸 모르고 로직 자체 문제인 줄 알고 거진 하루동안 싹 돌면서 수정했는데 맥이 다 풀리더라 ㅋㅋㅋㅋㅋㅋㅋ. 중복된 컴포넌트를 삭제해줌으로써 해결할 수 있었다. 덕분에 위 로직은 아래와 같이 깔끔하게 정리되었다.

 useEffect(() => {
    const listener = (event) => {
      const target = event.target.closest('.toggle');
      const clickedId = target ? +target.dataset.id : 0;
      const clickedSwipeIndex = target && +target.dataset.swipeIndex;
      dispatch(updateDataSet({ clickedId, clickedSwipeIndex }));
    };
   // ... 이외 생략
};


  1. Tooltip의 위치 표시하기
    돋보기 버튼의 위치에 따라 말풍선의 팁 방향과 풍선 자체의 위치를 동적으로 변경하도록 로직을 짰다. 기존 이미지의 width와 height의 절반보다 클 경우 방향과 위치가 바뀐다. Tooltip 컴포넌트가 인자로 받는 position은 vertical, horizontal 값을 가진다.
// Tooltip 컴포넌트
export type Vertical = 'bottom' | 'top';
export type Horizontal = 'left' | 'right';

export interface PositionType {
  veritcal?: Vertical;
  horizontal?: Horizontal;
}

const TooltipContain = ({ pointX, pointY }) => {
  return (
    <Tooltip position={getPositionOfTooltip(
            // @NOTE: pointX, poinY 반대로 넣어줘야 함
            pointY * rateOfImageDiff + theme.gap.image,
            pointX * rateOfImageDiff
          )}>
    </Tooltip>
  )
}
// position 객체를 리턴해주는 함수
export const getPositionOfTooltip = (pointX: number, pointY: number) => {
  // @NOTE: 기본값이 left, bottom
  // @NOTE: 기준값인 width(height)의 절반보다 point 값이 클 경우 top 혹은 left로 변화
  const veritcal: Vertical =
    pointY > theme.size.imageViewHeight / 2 ? 'top' : 'bottom';
  const horizontal: Horizontal =
    pointX > theme.size.imageViewWidth / 2 ? 'right' : 'left';

  return { veritcal, horizontal };
};
  • 실 이미지와 렌더링된 이미지 비율 계산하기
    데이터의 pointX와 pointY가 x축 y축이 아닌 y축 x축이여서 반대로 넣어줬다. 또한 각 point들은 실제 image 사이즈에 맞게 받아지므로 렌더링된 이미지의 비율로 계산하여 위치를 재조정해줘야 한다. 관련 로직은 useImageRate 훅에 작성했고 useImageRate는 ImageViewContent 컴포넌트에서 사용하였다. theme.gap.image는 받아온 데이터의 point들이 실제 서비스에 사용되는 point들과 11px 차이가 나는 것을 의미한다.

  • tooltip 위치 표시

    • 기본값은 left, bottom 형태이다.
    • vertical: bottom, horizontal: right
    • vertical: top, horizontal: right
    • vertical: top, horizontal: left


  1. 스와이프 구현하기
    프리온보딩 사전과제로 슬라이드를 깊게 다뤄봤기 때문에 이번에 구현할 땐 그렇게 어렵진 않았다. 다만 저번엔 너무 vanila JS 처럼 작성한 것 같아서 useSwipe 훅으로 분리해보았다. useSwipe 훅은 ImageViewSwiper 컴포넌트에서 사용했다. 한 가지 추가적으로 오른쪽으로 스와이프할 때 마지막 컴포넌트가 다 보인다면 딱 거기까지만 스와이프가 되도록 로직을 추가했다..
    이 부분은 실제 scroll이 되야 하는 width값과 list의 보이는 부분까지의 width를 뺀 부분을 overflowedX로 두었다.
  useEffect(() => {
    if (swipeRef.current) {
      const overflowedX =
        swipeRef.current?.scrollWidth -
        theme.size.boxWidth * dataLength -
        theme.gap.swiper * 2;

      setDragOverflowedX(overflowedX);
    }
  }, [swipeRef.current]);

만약 기존 translate3d X값에 오른쪽으로 스와이프한 값을 더했을 때 overflowedX 값보다 크다면 위치를 overflowedX 값으로 옮겨준다.

// ... 생략
if (-draggedX <= -boxWidth / 2) {
  // 드래그한 값이 아이템의 절반보다 클 때
  if (CheckDragOverflowLast(draggedX)) {
    // overflowed된 값보다 크다면 overflow된 값만큼만 이동
    setPosition(-overflowedX);
  } else {
    shiftSlide('right');
  }
// ... 생략

아쉽게도 데이터가 7개 뿐이라 스와이프하기에 충분히 많지 않아서 아이템 하나가 스와이프 되기 전에 overflowed가 되어버린다 ㅋㅋㅜ. 하지만 잘보면 아이템의 절반보다 크게 스와이프할 경우 옆으로 넘어가는 걸 확인할 수 있다.



  1. 공통 스타일 재사용하기
    마지막으로 공통된 스타일들은 함수로 만들어 재사용하였다. 실제 코드들은 styles/commonStyles에서 확인바란다.
// commonStyles
export const alignBackgroundImage = (size: string) => css`
  background-position: center;
  background-repeat: no-repeat;
  background-size: ${size};
`;

// Badge 컴포넌트 Style
export const BadgeInner = styled.div`
  position: absolute;
  top: 0;
  right: 5px;
  width: 24px;
  height: 28px;
  text-align: center;
  color: white;
  background-image: url(${badge});
  ${alignBackgroundImage('contain')};
  ${fontBadge};
`;

🌈 결론

써본 적없는 타입스크립트와 리덕스로 짧은 시간에 과제를 해야한다는 건 부담이었지만 막상 닥치면 뭐든 할 수 있다는 것을 배웠다 😇. 그 외에도 최대한 훅으로 만들거나 재사용성을 높이려고 노력한 프로젝트라 의미가 깊다.

profile
💪🏻 아는 걸 설명할 줄 아는 개발자 되기

0개의 댓글