🚀   새로운 블로그로 이전했습니다.

Drag Touch 뽀개기

2022.10.02
6분

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

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


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/