터치기기에서 드래그앤드랍을 다뤄보자

유키미아우·2023년 12월 12일
0

RN이 아닌 React로 개발한 나의 웹앱을 터치기반의 모바일기기에서도 이용가능하도록 최적화작업을 이어나가고 있다. 모바일기기 호환은 최근 웹뷰방식의 점유율이 올라가는 상황에서 꼭 한번 해보고 싶었던 작업이었다.
먼저 반응형UI 개발부터 발빠르게 마치고 이어서 드래그앤드랍 기능을 손볼 차례가 되자 많은 문제들이 속속 출현하였다.

내가 구현한 드래그앤드랍에 이용되는 이벤트들을 먼저 열거하면 다음과 같다.

  • mouseDown: 마우스 왼쪽버튼을 누르는 이벤트: 타겟의 좌표 갱신을 시작.
  • mouseMove: 마우스를 움직이는 이벤트: 타겟의 좌표를 실시간으로 갱신.
  • mouseUp: 마우스 왼쪽버튼에서 손가락을 떼는 이벤트: 타겟의 좌표 갱신을 종료.
  • ClientX, ClientY: 마우스의 실시간 위치 데이터. event객체 내부에서 접근가능.

데스크탑에서는 위 조합으로 잘 작동하였건만 iOS에서는 아무리 엘레멘트를 잡고 끌어도 꿈적도 하지 않아 난감했다. 그리고 이를 붙잡고 해결하는 과정에서 많은 새로운 배움을 얻었다.
시간이 지나도 꼭 기억하고 싶은 중요한 핵심을 기록해본다.

1. 데스크탑 vs 모바일기기

먼저 onMouseDown, onMouseUp는 작동을 하나 자연스러운 호환이 되지 않아 다소 버벅임이 있다. 또한 onMouseMove는 모바일기기에서 작동이 되지 않아 치명적이다.
따라서 모바일기기에서의 부드러운 조작경험을 위해서는 아래와 같은 터치전용 이벤트 리스너를 달아주어 유저입력을 핸들해줄 필요가 있다.

데스크탑모바일기기
onMouseDownonTouchStart
onMouseMoveonTouchMove
onMouseUponTouchEnd

2. mouse event vs touch event

touchMove 이벤트 핸들러도 달아주었겠다 기존 event를 인자로 받는 함수를 재사용하여 clientX와 clientY를 얻어낼 수 있을까?
아쉽지만 이벤트의 종류가 다르기 때문에 이벤트 객체의 구조와 뎁스도 마찬가지로 다르다.
또한 타입스크립트를 이용할시에는 event 타입지정도 React.MouseEvent과 React.TouchEvent으로 다른 점에 유의하자.

onMouseMoveonTouchMove
X좌표event.clientXevent.touches[0].clientX
Y좌표event.clientYevent.touches[0].clientY

데스크탑의 마우스이벤트는 입력의 개수가 한개이다.
그러나 터치기기는 어떤가? 엄지 검지를 활용해서 줌인 조작을 할 때도 있듯이, 입력의 개수가 한개도 여러개가 될 수도 있다. 따라서 event내부에 touches 배열이 존재하며 0번 인덱스에 접근하여 clientX, clientY를 얻을 수 있다.

3. 스크롤아 가만히 좀 있어줘

이제 드래그앤드랍이 가능하게 되자 다음으로 또 문제가 발생했다.
브라우저의 native 스크롤링과, 뒤로가기 새로고침 스와이프 등이 함께 작동하며 유저경험이 엉망이 된 것이다.


🔺 😵‍💫 어질어질.. 정상적인 조작을 전혀 할 수 없다.

이를 해결하기 위해 다방면으로 조사했다. e.preventDefault();를 해보라는 답변이 많이 눈에 띄었으나 안타깝게도 적용해도 차도가 없었다. 그러던 중 다른 자료에서 touch-action이라는 css 어트리뷰트를 알게 되었다.
TailwindCSS에서는 touch-옵션이름으로 쓰인다.

https://tailwindcss.com/docs/touch-action#setting-the-touch-action
🔺 모바일 기기를 통해 접속해보면 좋다.

쉽게 말해 터치하여 움직일 시의 스크롤 처리를 제어할 수 있는 것이다. 요소의 특성에 따라 좌우 스와이프, 상하 스와이프 등을 제한해주고 싶을 때가 있을텐데 그럴 때 적용하면 편리할 것 같았다. 나의 케이스는 상하좌우 스크롤을 제한시켜주어야 하기 때문에 touch-none가 딱이라고 생각해 적용했다.

touch-none은 터치되는 타겟 엘리먼트에 적용되어야 한다. 이웃이나 상위 컴포넌트가 아니다! 이 부분이 헷갈릴 수 있으므로 유의하자.


🔺 touch-none 적용 후 스크롤이 freeze되어 원하는 엘리먼트만 깔끔하게 움직이는 모습이다.

4. 키보드가 왜 안 열릴까

input을 터치해보았더니 키보드가 안 열리는 현상을 발견했다.
이번에도 모바일에서의 호환성 문제가 야기하는 특수한 에러인걸까? 싶어 걱정이 앞섰다. 일단 확실한 검증을 해보기 위하여 Netlify를 통해 배포되어있는 실험용 repo에 input을 만들어 모바일기기에서 접속하고 터치해보았다.
그러자 웬걸, 한번의 터치로 멀쩡히 키보드가 열리는 것이 아닌가?..
따라서 프로젝트의 코드나 css에 의한 문제점임을 확신했다.


🔺 아무리 input을 터치해도 키보드가 올라오지 않는 현상

onTouchStart 혹은 onTouchMove시에 "readonly" attribute를 제거하는 로직을 넣어보라는 글. iOS의 배터리절약 모드가 문제라는 글. 등등 다양한 해답들을 찾아보며 많은 삽질을 했다.

고민하다 지쳐 허망하게 터치를 반복해보던 도중, 아래 두가지 현상을 발견했다.

  • 검지와 중지를 이용해 따닥하고 빠르게 2회 터치하면 키보드가 뜬다?
  • 터치할 때마다 다른 컴포넌트에 미세한 flickering이 있다?

이를 통해서 리렌더링이 원인이 아닐까 유추하였고 콘솔찍기 디버깅 신공을 이용해 범인을 좁혔다.

범인은 바로
...
..
.

onMouseUP 이벤트로 인한 리렌더링이었다.

마우스의 버튼클릭이 종료되는 동시에 드래그를 마치도록 달아주었었던 이벤트핸들러인데 부모태그에 달려있어 드래그 영역 안팎 어디서든지 작동되도록 구현했었다. 따라서 input태그 위에서도 작동한다는 뜻이 된다.

문제는 핸들러가 트리거하는 함수를 통해 "isDragging" state를 초기화 시켜주면, 그 state를 바라보는 하위 컴포넌트가 모두 리렌더링 되는 점이었다. 자식들 중 하나인 input태그 역시 함께 리렌더링되었던 바람에 키보드가 열릴 겨를이 없었던 것.

클릭/터치 시 무조건적으로 작동하는 onMouseUP을 드래그 중이었을 때에만 작동하도록 삼항연산자를 적용해주었다.
그러자 input태그 터치에 의한 리렌더링이 멈추면서 문제가 즉각 해결되었다!


🔺 한번의 터치로 잘 올라와준다. 키보드가 이렇게 반가울수가?

오늘로써 대부분의 기능이 모바일에서도 잘 작동되게 되었으나 아직 복잡한 기능 몇가지가 남았다. 당황하지 않고 차근차근 해결해나가자.

profile
능동적인 마음

0개의 댓글