기능구현 - Drag Carousel

치맨·2023년 7월 27일
3

기능구현

목록 보기
5/9
post-thumbnail

목차


Drag Carousel 구현하기

  • 여러 사이트 혹은 모바일 앱을 사용하다 보면 캐러셀을 넘길 때 버튼을 클릭해서 넘기는 경우도 있고, Drag를 통해 넘기는 경우도 있습니다.
    아래의 영상은 레뷰라는 사이트입니다.

  • 한 번 만들어 보고 싶다는 생각이 들어서 새로 진행하는 프로젝트에서 기능을 구현해 봤습니다.

  • 저는 우선 MouseEvent를 사용하여 구현했습니다. Drag Event 보다 MouseEvent가 호환성이 더 좋다고 하며(최신 브라우저는 상관없지만, 구식 브라우저는 Drag가 안되는것도 있다고 함), 뭔가 DragEvent는 드래그 앤 드랍에 사용해야 할 것만 같은 느낌이라 MouseEvent를 사용했습니다.

  • MouseEvent에서 mousedown, mousemove, mouseup 3가지의 event를 사용해서 구현했습니다.

    • mousedown : 마우스를 눌렀을 때
    • mousemove : 마우스를 움직일 때
    • mouseup : 마우스를 땔 때

전체코드

  • 우선 전체코드를 먼저 공유한 뒤, 차근차근 알아보겠습니다.
 
  const Carousel = ({ items, itemWidth = 150 }: CarouselProps) => {
  const carouselItemsRef = useRef<HTMLDivElement>(null);

  const [isDragging, setIsDragging] = useState(false);
  const [startPosition, setStartPosition] = useState(0);
  const [endPosition, setEndPosition] = useState(0);
  const [isMobile, setIsMobile] = useState(false);
  const navigate = useNavigate();

  const moveTowardX = (movedDistance: number) => {
    const totalCarouselWidth = itemWidth * items.length + ITEM_MARGIN * (items.length - 1);
    const [minPosition, maxPosition] = [-totalCarouselWidth + itemWidth, 0];

    if (movedDistance < minPosition) return minPosition;
    if (movedDistance > maxPosition) return maxPosition;
    return movedDistance;
  };

  const handleMouseDown = (event: MouseEvent) => {
    event.preventDefault();
    setStartPosition(event.clientX);
    setIsDragging(true);
  };

  const handleMouseMove = (event: MouseEvent) => {
    if (!isDragging) return;
    const movedDistance = moveTowardX(endPosition - startPosition + event.clientX);
    if (carouselItemsRef.current) carouselItemsRef.current.style.transform = `translateX(${movedDistance}px)`;
  };

  const handleMouseUp = (event: MouseEvent) => {
    if (!isDragging) return;
    const movedDistance = moveTowardX(endPosition - startPosition + event.clientX);
    setEndPosition(movedDistance);
    setIsDragging(false);
  };

  useEffect(() => {
    const carouselItems = carouselItemsRef.current;

    carouselItems?.addEventListener('mousedown', handleMouseDown);
    carouselItems?.addEventListener('mousemove', handleMouseMove);
    carouselItems?.addEventListener('mouseup', handleMouseUp);

    return () => {
      carouselItems?.addEventListener('mousedown', handleMouseDown);
      carouselItems?.removeEventListener('mousemove', handleMouseMove);
      carouselItems?.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging]);

  useEffect(() => {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    isMobile ? setIsMobile(true) : setIsMobile(false);
  }, []);

  const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
    if (Math.floor(startPosition) === e.clientX) navigate(items[Number(e.currentTarget.dataset.id)].link);
  };

  return (
    <Wrapper $ismobile={isMobile}>
      <div ref={isMobile === true ? null : carouselItemsRef}>
        <ProductCard items={items} onClick={handleClick} itemWidth={itemWidth} />
      </div>
    </Wrapper>
  );
};

export default Carousel;
  1. 캐러셀 Container에 useRef를 걸어주고, event를 적용시켜줍니다.
    1-1 : mousemove Event를 window에 걸어준 이유는 움직이고, 마우스를 때는 부분을 ref에 걸어준다면 캐러셀 Container 안에서 아이템을 클릭한 상태로 캐러셀 Container 외부에서 마우스를 땐다면 마우스를 땐 시점이 아닌 캐러셀 Container를 벗어난 지점에서 종료해버리기 때문에 문제가 발생했기 때문입니다.
    드래그를 해서 캐러셀 Container 밖에서 마우스를 땐 뒤 다시 드래그할때 잠시 끊기는 에러때문에 window객체에 event를 걸어줍니다.

1-2 : mouseup Event를 window에 걸어준 이유는 움직이고, ref에 event를 걸어준다면 캐러셀 Container를 벗어나면 event가 종료되기 때문에 마우스가 계속 클릭한 상태로 유지되는 문제가 발생합니다. 따라서 마우스를 캐러셀 Container 밖으로 이동후, 다시 돌아오면 클릭하지 않아도 계속 드래그가 되는 문제가 발생합니다.

1-3 : event가 메모리 누수의 원인이 될 수도 있기 때문에 useEffect의 cleanUp 을 이용하여 event를 삭제해줍니다.

  useEffect(() => {
    const carouselItems = carouselItemsRef.current;

    carouselItems?.addEventListener('mousedown', handleMouseDown);
    carouselItems?.addEventListener('mousemove', handleMouseMove);
    carouselItems?.addEventListener('mouseup', handleMouseUp);

    return () => {
      carouselItems?.addEventListener('mousedown', handleMouseDown);
      carouselItems?.removeEventListener('mousemove', handleMouseMove);
      carouselItems?.removeEventListener('mouseup', handleMouseUp);
    };
  }, [isDragging]);
  1. 이제 마우스를 클릭했을때, 움직일때, 마우스를 땔때 함수를 알아보겠습니다.
// 캐러셀의 맨왼쪽 혹은, 맨 오른쪽 그 이상은 넘어가지 않도록 최소값과, 최대값을 통해 최소값보다 적어지거나, 최대값보다 많아지면 종료를 시켜줍니다.
 const moveTowardX = (movedDistance: number) => {
    const totalCarouselWidth = itemWidth * items.length + ITEM_MARGIN * (items.length - 1);
    const [minPosition, maxPosition] = [-totalCarouselWidth + itemWidth, 0];

    if (movedDistance < minPosition) return minPosition;
    if (movedDistance > maxPosition) return maxPosition;
    return movedDistance;
  };

// 드래그 캐러셀을 만드는 경우에는 클릭 이벤트의 기본 동작을 막아서 커스텀 드래그 동작을 처리하는 방식이 일반적이라고 합니다. 
// 또한 시작지점을 찾아주고, 드래그 할것입니다~ 라고 하는 isDragging을 true로 만들어줍니다.
  const handleMouseDown = (event: MouseEvent) => {
    event.preventDefault();
    setStartPosition(event.clientX);
    setIsDragging(true);
  };

// isDragging이 false가 된다면 즉시 종료를 해줘 에러를 방지합니다. 
// 움직이는 경우 끝지점(마우스를 때는 시점) - 시작지점(마우스를 클릭한 시점) + 현재 마우스의 clientX값을 찾아주고, transform의 translateX를 통해 움직여줍니다. 
  const handleMouseMove = (event: MouseEvent) => {
    if (!isDragging) return;
    const movedDistance = moveTowardX(endPosition - startPosition + event.clientX);
    if (carouselItemsRef.current) carouselItemsRef.current.style.transform = `translateX(${movedDistance}px)`;
  };

// 또한 isDragging이 false가 된다면 즉시 종료를 해줘 에러를 방지합니다. 
// endPosition 즉 마우스를 떈 지점을 endPosition state값으로 담아주고, isDragging을 false로 바꿔 드래그를 종료해줍니다. 
  const handleMouseUp = (event: MouseEvent) => {
    if (!isDragging) return;
    const movedDistance = moveTowardX(endPosition - startPosition + event.clientX);
    setEndPosition(movedDistance);
    setIsDragging(false);
  };
  • 잘 동작하는걸 확인할 수 있습니다.

모바일의 경우 dragEvent처리

  • 모바일의 경우 mouseEvent가 동작하지 않습니다.

  • 따라서 touch event를 사용하거나, css를 사용하여 scroll을 통해 drag를 처리해줍니다.

  • 저는 css로 처리를 해보았습니다. 만약 touch Event로 처리하는 경우 mouseEvent와 비슷하지만, mousedown, mousemove, mouseup 이벤트를 touchstart, touchmove, touchend 이벤트를 사용하며, event.clientX대신 event.touches[0].clientX 이런식으로 접근하는 차이 뿐 나머지는 같습니다.

  1. navigator.userAgent를 통해 모바일인지 데스크탑인지 확인하여 isMobile이라는 state값에 담아줍니다.
 useEffect(() => {
    const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent);
    isMobile ? setIsMobile(true) : setIsMobile(false);
  }, []);
  1. css를 통해 mobile일 경우 css를 추가해줍니다.
interface IIsmobile {
  $ismobile: boolean;
}

const Wrapper = styled.div<IIsmobile>`
  overflow: hidden;

  ${({ $ismobile }) =>
    $ismobile &&
    css`
      overflow-x: auto;
      overflow-y: hidden;
      white-space: nowrap;
      -webkit-overflow-scrolling: touch;
      -ms-overflow-style: none;
      scrollbar-width: none;
    `}
`;
  • 잘 동작하는걸 확인할 수 있습니다.

많이 삽질한 부분

  • 위에서 이미 언급했지만, 사실 mousemove, mouseup event를 window가 아닌 ref에 걸어줘서 발생한 문제들을 통해 몇시간을 삽질했으며, 모바일 버전에서는 Drag가 동작하지 않는다는 걸 처음 알았고, 이에 따라 touch Event와 css를 통해 적용하는 부분을 찾고, 적용하는데 또한 몇시간 삽질과 공부를 병행했었습니다. 혹시 Drag 캐러셀을 구현하신다면 이 부분을 주의깊게 처리하시면 저보다는 시간을 더욱 아낄 수 있을것입니다.
profile
기본기가 탄탄한 개발자가 되자!

0개의 댓글