Drag Touch 뽀개기

bepyan·2022년 10월 3일
19

DND 마스터 시리즈

목록 보기
2/7
post-thumbnail

이전 포스트에서 만든 유틸함수에 큰 문제점이 있다.
바로 모바일 기기에서 동작하지 않다는 것이다.

힘들게 개발한 것이 모바일에서 동작하지 않다니 ㅠ퓨ㅜㅠ

TL;DR
모바일 기기에서는 MouseEvent 대신 TouchEvent를 사용한다.
window.matchMedia('(hover: none) and (pointer: coarse)').matches를 통해 모바일 기기인지 여부를 파악한다.
기기에 따라 mousedown 혹은 touchdown 이벤트를 등록해준다.




사전 지식 — Touch Event

모바일 기기에서는 MouseEvent 대신 TouchEvent가 발생된다.
따라서 MouseEvent로 등록되었던 이벤트가 실행되지 않았던 것이다.
다행히 TouchEvent에서도 우리가 원하는 동작을 바로 찾을 수 있다.

mousedowntouchdown
mousemovetouchmove
mouseuptouchend

TouchEvent의 속성 중
touches — 모든 접촉점의 터치 리스트
targetTouches — 현재 이벤트 타겟에서 시작된 터치 리스트
changedTouches — 이전 이벤트에 할당된 모든 접촉점의 터치 리스트

터치 스크린 특성상 여러 터치 이벤트가 동시 실행될 수 있어서 터치 리스트를 반환하는 것 같다.
일반적으로 첫 Touch 이벤트를 사용하면 될 것이다.

정확히 차이에 대해 와닿진 않지만
움직일 때는 touches[0], 손을 땠을 때는 changedTouches[0]를 사용하도록 하자.




기본 원리 — touch 이벤트 등록

이전 mouse 등록 절차와 같으니 자세한 설명을 생략하겠다.

<Boundary
  onTouchStart={(touchEvent) => {
    const touchMoveHandler = (moveEvent: TouchEvent) => {
      setPosition({
        x: moveEvent.touches[0].pageX - touchEvent.touches[0].pageX,
        y: moveEvent.touches[0].pageY - touchEvent.touches[0].pageY,
      });
    };
    const touchEndHandler = () => {
      document.removeEventListener('touchmove', touchMoveHandler);
    };

    document.addEventListener('touchmove', touchMoveHandler);
    document.addEventListener('touchend', touchEndHandler, { once: true });
  }}
/>

onMouseStart, onTouchStart 둘다 등록하긴 너무나 귀찮다...
MouseEvent, TouchEvent을 동시 등록할 수 있는 유틸을 만들어 보자.
그럼 브라우저 환경에 따라 MouseEvent를 등록할지 TouchEvent를 등록할지 판별할 수 있어야 한다.




심화 적용 — pc와 mobile를 구분

일반적으로 디바이스 크기로 pc와 mobile을 구분하지만
pc에서 작은 화면으로 볼 수도 있고 13인치 iPad를 사용할 수도 있다.

이를 해결해주는 아주 잘 정리된 블로그가 있다.
hover, pointer 쿼리를 이용하면 모바일 기기를 구분할 수 있다.

출처 https://paperblock.tistory.com/164


그렇다면 이 CSS Media Queries를 어떻게 사용할 수 있을까?

바로 window api, matchMedia를 사용하는 것이다.
아래 코드로 “현재 화면이 미디어쿼리의 범위에 들어가는지” 확인할 수 있다.

window.matchMedia('(max-width: 600px)').matches; // boolean

여러 쿼리를 같이 확인하고 싶으면 and 를 붙이면 된다.

window.matchMedia('(hover: none) and (pointer: coarse)').matches

NextJS에서는 기본적으로 SSR하기 때문에 window가 undefined할 수 있다.
따라서 아래와 같이 코드를 작성해주면 사용자의 화면이 터치 스크린인지 확인할 수 있다.

export const isTouchScreen =
  typeof window !== 'undefined' 
  && window.matchMedia('(hover: none) and (pointer: coarse)').matches;



심화 응용 — 최종 코드

utils/registDragEvent.ts
아직 부족한 부분이 많지만 상황에 맞춰 잘 수정하면 될 것이다.

const isTouchScreen =
  typeof window !== 'undefined' && window.matchMedia('(hover: none) and (pointer: coarse)').matches;

export default function registDragEvent({
  onDragChange,
  onDragEnd,
  stopPropagation,
}: {
  onDragChange?: (deltaX: number, deltaY: number) => void;
  onDragEnd?: (deltaX: number, deltaY: number) => void;
  stopPropagation?: boolean;
}) {
  if (isTouchScreen) {
    return {
      onTouchStart: (touchEvent: React.TouchEvent<HTMLDivElement>) => {
        if (stopPropagation) touchEvent.stopPropagation();

        const touchMoveHandler = (moveEvent: TouchEvent) => {
          if (moveEvent.cancelable) moveEvent.preventDefault();
          
          const deltaX = moveEvent.touches[0].pageX - touchEvent.touches[0].pageX;
          const deltaY = moveEvent.touches[0].pageY - touchEvent.touches[0].pageY;
          onDragChange?.(deltaX, deltaY);
        };

        const touchEndHandler = (moveEvent: TouchEvent) => {
          const deltaX = moveEvent.changedTouches[0].pageX - touchEvent.changedTouches[0].pageX;
          const deltaY = moveEvent.changedTouches[0].pageY - touchEvent.changedTouches[0].pageY;
          onDragEnd?.(deltaX, deltaY);
          document.removeEventListener('touchmove', touchMoveHandler);
        };

		document.addEventListener('touchmove', touchMoveHandler, { passive: false });
        document.addEventListener('touchend', touchEndHandler, { once: true });
      },
    };
  }

  return {
    onMouseDown: (clickEvent: React.MouseEvent<Element, MouseEvent>) => {
      if (stopPropagation) clickEvent.stopPropagation();

      const mouseMoveHandler = (moveEvent: MouseEvent) => {
        const deltaX = moveEvent.pageX - clickEvent.pageX;
        const deltaY = moveEvent.pageY - clickEvent.pageY;
        onDragChange?.(deltaX, deltaY);
      };

      const mouseUpHandler = (moveEvent: MouseEvent) => {
        const deltaX = moveEvent.pageX - clickEvent.pageX;
        const deltaY = moveEvent.pageY - clickEvent.pageY;
        onDragEnd?.(deltaX, deltaY);
        document.removeEventListener('mousemove', mouseMoveHandler);
      };

      document.addEventListener('mousemove', mouseMoveHandler);
      document.addEventListener('mouseup', mouseUpHandler, { once: true });
    },
  };
}

_ 22.10.10 추가

모바일 기기에서 touch를 통해서 scroll를 내리게 된다.
따라서 drag하면서 scroll이 되는 버그가 발생하게 된다...

이를 해결해주기 위해

const touchMoveHandler = (moveEvent: TouchEvent) => {
  if (moveEvent.cancelable) moveEvent.preventDefault();
};

document.addEventListener('touchmove', touchMoveHandler, { passive: false });


실제 동작은 아래 링크에서 볼 수 있다.
https://dnd-playground.vercel.app/

style 및 전체 코드는 아래 깃허브에서 살펴보면 될 것 같다.
https://github.com/bepyan/dnd-playground

글 이전
https://bepyan.github.io/blog/dnd-master/2-drag-touch-event

profile
쿠키 공장 이전 중 🚛 쿠키 나누는 것을 좋아해요.

1개의 댓글

comment-user-thumbnail
2023년 7월 5일

안녕하세요! 블로그 참고하면서 touch event 구현 중입니다. 게시글이 많은 도움이 돼서 감사 인사 드립니다!! 그런데 저는 안드로이드에서 touchmove 시 뚝뚝 끊기는 현상이 발생하는데 혹시 같은 문제를 겪으셨는지, 겪었다면 어떻게 해결하셨는지 여쭤봐도 될까요?

답글 달기