데스크톱에서 드래그 앤 드롭이 어느 정도 자연스럽게 돌아가기 시작하면, 거의 바로 다음 질문이 나옵니다. “이제 모바일에서도 되게 만들 수 있을까?” 겉으로만 보면 그렇게 어려워 보이지 않습니다. 손가락으로 항목을 눌러서 옮기면 될 것 같기 때문입니다. 그런데 실제로 붙여보면 이 단계에서 다시 분위기가 달라집니다.

데스크톱에서는 마우스로 항목을 끌고 놓는 흐름이 비교적 분명합니다. 반면 모바일에서는 손가락이 스크롤 도구이기도 하고, 클릭 도구이기도 하고, 드래그 도구이기도 합니다. 같은 입력 장치가 여러 역할을 겸하고 있기 때문에, 데스크톱에서 만든 흐름을 그대로 믿으면 갑자기 스크롤이 막히거나, 드롭 대상이 제대로 안 잡히거나, 손가락 아래에 가려진 위치를 제대로 읽지 못하는 문제가 생기기 쉽습니다.

이번 글에서는 왜 데스크톱 드래그가 모바일에서 그대로 안 맞는지부터 시작해서, 터치 기반 재정렬을 처음 붙일 때 어떤 규칙부터 정해야 하는지, 그리고 입문자라면 왜 처음부터 복잡한 모바일 드래그를 욕심내기보다 드래그 핸들터치 상태 분리부터 이해하는 편이 좋은지를 차근차근 정리해보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
데스크톱에선 잘 되던 드래그가 모바일에선 어색하다 손가락은 마우스와 다르게 스크롤과 드래그를 동시에 겸한다 마우스 기준 규칙을 그대로 옮기지 말고 터치 규칙을 따로 본다
드래그하려고 했는데 페이지가 먼저 스크롤된다 터치 이동을 브라우저가 스크롤로 받아들이고 있다 어디서부터 드래그를 시작할지 명확한 규칙이 필요하다
드롭 위치가 이상하게 잡힌다 손가락 아래 대상 요소를 마우스처럼 바로 읽기 어렵다 현재 터치 좌표로 실제 대상 요소를 다시 찾는 흐름이 필요하다
모바일용으로 고치다 보니 코드가 더 복잡해진다 마우스와 터치의 차이를 구분하지 않고 한꺼번에 섞었다 첫 버전은 터치용 규칙을 따로 분명하게 두는 편이 낫다

이번 글의 핵심 한 줄
모바일 재정렬은 마우스를 손가락으로 바꾸는 문제가 아니라, 스크롤과 드래그가 같은 입력 안에서 충돌하는 환경을 다시 설계하는 문제에 가깝다.


목차

  1. 왜 데스크톱 드래그가 모바일에서 그대로 안 맞을까
  2. 마우스 이벤트와 터치 이벤트는 무엇이 다른가
  3. 첫 모바일 버전에서 먼저 정해야 할 규칙
  4. 가장 현실적인 첫 접근: 드래그 핸들 + touchstart, touchmove, touchend
  5. 스크롤과 드래그가 충돌할 때는 어떻게 볼까
  6. 초보자가 자주 하는 실수 6가지
  7. 마무리

왜 데스크톱 드래그가 모바일에서 그대로 안 맞을까

데스크톱에서 드래그는 비교적 단순합니다. 마우스를 누르고 움직이고 놓는 흐름이 있고, 사용자는 보통 포인터가 무엇을 가리키는지도 눈으로 명확하게 확인할 수 있습니다. 반면 모바일에서는 손가락 자체가 화면 일부를 가립니다. 즉, 지금 내가 정확히 어느 항목의 위쪽 절반에 있는지 아래쪽 절반에 있는지 보기가 생각보다 쉽지 않습니다.

게다가 손가락은 스크롤을 담당하는 입력이기도 합니다. 사용자는 목록을 위아래로 읽기 위해 스와이프를 하고, 같은 동작처럼 보이는 제스처로 항목을 옮기고 싶어 하기도 합니다. 이 둘을 코드가 구분하지 못하면, 순서 변경을 하려던 손동작이 스크롤로 처리되거나 반대로 스크롤하려던 손동작이 드래그 시작으로 해석될 수 있습니다.

데스크톱과 모바일은 입력 환경 자체가 다르다
구분 데스크톱에서 비교적 쉬운 점 모바일에서 갑자기 어려워지는 점
입력 도구 마우스는 클릭과 드래그 역할이 비교적 분명하다 손가락은 클릭, 스크롤, 드래그 역할이 겹친다
정밀도 포인터 위치를 비교적 정확하게 볼 수 있다 손가락이 화면을 가려 대상 판단이 더 어렵다
스크롤 충돌 보통 휠이나 트랙패드가 따로 있다 같은 손동작이 스크롤과 드래그 후보가 된다
상태 피드백 포인터와 표시선을 동시에 보기 쉽다 표시선과 대상 요소가 손가락에 가려질 수 있다

즉, 모바일 재정렬이 어려운 이유는 이벤트 이름이 다르기 때문만은 아닙니다. 그보다 먼저 입력 환경의 성격이 다르다는 점이 더 중요합니다. 이걸 이해하면 왜 첫 모바일 버전에서는 규칙을 더 명확하게 잡아야 하는지도 자연스럽게 보입니다.

핵심 포인트
모바일에서는 “손가락이 지금 스크롤을 하려는가, 순서를 바꾸려는가”를 앱이 먼저 구분해줘야 한다.


마우스 이벤트와 터치 이벤트는 무엇이 다른가

입문자 입장에서 가장 먼저 헷갈리는 건 이벤트 이름입니다. 데스크톱에서는 dragstart, dragover, drop 같은 흐름을 따라왔는데, 모바일로 오면 touchstart, touchmove, touchend 같은 이름이 보입니다. 이름이 달라지니 전혀 다른 세계처럼 느껴질 수 있습니다. 그런데 역할로 보면 생각보다 크게 다르지 않습니다.

중요한 건 “이름을 외우는 것”보다 각 단계가 무슨 책임을 맡는지를 이해하는 것입니다.

데스크톱 드래그 흐름과 모바일 터치 흐름을 역할로 비교해보기
역할 데스크톱에서 자주 보던 것 모바일에서 보게 되는 것
시작 dragstart touchstart
이동 중 추적 dragover touchmove
놓기 또는 확정 drop touchend
중단 또는 정리 dragend touchend 또는 touchcancel

모바일에서 특히 신경 써야 하는 점은 touchmove 안에서 지금 손가락 아래에 실제로 어떤 요소가 있는지 다시 찾는 과정이 필요하다는 것입니다. 데스크톱에서는 포인터 기준으로 흐름이 비교적 자연스럽게 이어지지만, 터치에서는 사용자의 손가락 아래에 target이 가려질 수 있고, 이벤트를 처음 받은 요소와 현재 아래 있는 요소가 다를 수도 있습니다.

그래서 모바일 첫 버전에서는 보통 아래 같은 상태를 따로 들고 갑니다.

let touchDraggedId = null;

let touchTargetId = null;
let touchDropPosition = null;
let isTouchSorting = false;

이 상태들은 모두 임시 상태입니다. 실제 데이터인 todos를 바로 바꾸는 것이 아니라, 지금 어떤 항목을 잡았는지, 어디로 놓으려는지, 지금 정렬 모드가 켜져 있는지를 기억하는 데 쓰입니다. 이 구분은 데스크톱 드래그에서도 중요했지만, 모바일에서는 더 중요해집니다. 왜냐하면 손가락 이동 중에 발생하는 임시 상태 변화가 훨씬 많기 때문입니다.

쉽게 이해하면
모바일 터치 정렬도 결국 시작, 이동 중 추적, 놓기, 정리의 흐름이다. 이름이 달라졌을 뿐, 무슨 역할을 맡는지는 크게 다르지 않다.


첫 모바일 버전에서 먼저 정해야 할 규칙

모바일 재정렬은 처음부터 규칙을 좁게 잡는 편이 훨씬 낫습니다. 데스크톱에서는 조금 투박해도 마우스로 커버가 되지만, 모바일에서는 같은 애매함이 곧바로 불편함으로 느껴집니다. 그래서 “어디를 누르면 정렬 시작인지”, “지금은 스크롤인지 정렬인지”, “어떤 모드에서만 허용할지”를 먼저 정해두는 편이 좋습니다.

입문자 기준으로는 아래 네 가지가 특히 중요합니다.

  • 항목 전체를 잡게 하지 말고 드래그 핸들을 둔다
    스크롤하려고 터치했는데 정렬이 시작되는 문제를 줄일 수 있습니다.
  • custom 모드에서만 재정렬을 허용한다
    최신순이나 오래된순 같은 자동 정렬 모드에서는 사용자가 방금 옮긴 결과를 화면에서 바로 이해하기 어렵습니다.
  • 전체 보기에서만 먼저 허용한다
    검색이나 필터가 걸린 상태에서는 숨겨진 항목 때문에 결과가 더 헷갈릴 수 있습니다.
  • 첫 버전에서는 길게 누르기보다 핸들 시작을 우선한다
    길게 누르기까지 같이 붙이면 규칙이 하나 더 늘어서 입문자에게는 오히려 흐름이 흐려질 수 있습니다.
첫 모바일 버전에서 먼저 정하면 좋은 규칙
규칙 왜 이렇게 두는가
핸들에서만 정렬 시작 항목 본문 터치는 스크롤과 클릭에 남겨둘 수 있다
custom 모드 한정 사용자 순서와 자동 정렬 규칙이 섞이는 혼란을 줄일 수 있다
전체 보기 한정 보이는 항목만 움직이는 상황과 전체 순서가 섞이는 걸 막을 수 있다
첫 버전은 간단한 규칙 우선 길게 누르기, 진동, 자동 스크롤까지 한꺼번에 붙이면 디버깅이 어려워진다

이 규칙이 중요한 이유는 단순합니다. 모바일에서는 사용자의 손동작이 아주 다양한 의미를 가질 수 있기 때문입니다. 그래서 무엇이 정렬 시작인지 앱이 먼저 분명하게 정해줘야 합니다. 첫 버전에서는 기능을 줄이는 것이 오히려 기능을 제대로 이해하는 지름길에 가깝습니다.

실전 감각으로 정리하면
모바일 재정렬은 “무엇을 할 수 있게 만들까”보다 먼저 “무엇은 아직 못 하게 둘까”를 정하는 쪽이 훨씬 중요할 때가 많다.


가장 현실적인 첫 접근: 드래그 핸들 + touchstart, touchmove, touchend

입문자에게 가장 무난한 첫 접근은 항목 전체를 드래그 영역으로 두지 않고, 드래그 핸들을 따로 두는 방식입니다. 예를 들어 각 항목 오른쪽에 작은 순서 이동 버튼을 두고, 그 부분에서만 터치 정렬이 시작되게 만드는 것입니다. 이렇게 하면 스크롤과 충돌하는 범위를 크게 줄일 수 있습니다.

HTML 구조는 아래처럼 단순하게 둘 수 있습니다.

<li class="todo-item" data-todo-id="101">

순서 이동
장보기

핵심은 드래그 핸들에만 터치 시작 이벤트를 붙이고, 이동 중에는 현재 손가락 아래에 있는 요소를 다시 찾는 것입니다. 이때 초보자에게 자주 도움이 되는 방식이 document.elementFromPoint입니다. 현재 터치 좌표 아래에 어떤 요소가 있는지를 찾아서, 그 요소가 어느 항목인지 다시 확인하는 식입니다.

let touchDraggedId = null;

let touchTargetId = null;
let touchDropPosition = null;
let isTouchSorting = false;

function getDropPositionFromTouch(touch, element) {
const rect = element.getBoundingClientRect();
const middleY = rect.top + rect.height / 2;

return touch.clientY < middleY ? 'before' : 'after';
}

function handleTouchStart(event, todoId) {
isTouchSorting = true;
touchDraggedId = todoId;
}

function handleTouchMove(event) {
if (!isTouchSorting) return;

const touch = event.touches[0];
const currentElement = document.elementFromPoint(touch.clientX, touch.clientY);
const item = currentElement && currentElement.closest('[data-todo-id]');

if (!item) return;

touchTargetId = Number(item.dataset.todoId);
touchDropPosition = getDropPositionFromTouch(touch, item);

event.preventDefault();
}

function handleTouchEnd() {
if (isTouchSorting && touchDraggedId !== null && touchTargetId !== null && touchDropPosition) {
moveTodoByDropPosition(touchDraggedId, touchTargetId, touchDropPosition);
}

resetTouchSorting();
}

function resetTouchSorting() {
touchDraggedId = null;
touchTargetId = null;
touchDropPosition = null;
isTouchSorting = false;
}

이 코드에서 초보자가 꼭 봐야 할 부분은 세 군데입니다.

  1. touchstart에서는 실제 데이터 변경이 아니라 “누구를 움직일지”만 잡습니다.
  2. touchmove에서는 현재 손가락 아래 target과 before·after를 계속 갱신합니다.
  3. touchend에서만 실제 재배치를 실행합니다.

이 흐름이 중요한 이유는, 모바일에서도 결국 임시 상태실제 데이터 변경을 나눠서 보는 습관이 그대로 필요하기 때문입니다. 손가락이 움직이는 동안에는 상태만 추적하고, 손을 뗀 순간에만 진짜 순서 변경을 확정하는 편이 훨씬 덜 흔들립니다.

입문자 기준으로 특히 중요한 점
모바일 드래그도 결국은 “누구를 잡았는가”, “어디 위에 있는가”, “손을 뗐을 때 어떻게 반영할 것인가”를 나누는 문제다. 한 번에 다 하려 하면 오히려 더 헷갈린다.


스크롤과 드래그가 충돌할 때는 어떻게 볼까

모바일 재정렬에서 가장 자주 체감되는 불편은 사실 순서 계산보다 스크롤 충돌입니다. 사용자는 목록을 읽기 위해 위아래로 문지르는데, 앱은 그것을 드래그로 해석할 수도 있습니다. 반대로 사용자는 순서를 바꾸고 싶었는데 브라우저가 먼저 스크롤을 해버릴 수도 있습니다.

그래서 여기서는 “무조건 preventDefault를 붙이면 된다”처럼 생각하면 오히려 더 꼬입니다. 전체 목록에서 터치 이동을 전부 막아버리면, 이제 드래그는 되더라도 페이지 스크롤이 너무 불편해질 수 있기 때문입니다.

스크롤과 드래그 충돌을 볼 때 먼저 구분할 것
상황 왜 불편해지나 더 나은 방향
항목 전체를 드래그 시작 영역으로 둔다 스크롤 의도와 정렬 의도가 쉽게 충돌한다 핸들 영역만 정렬 시작점으로 두는 편이 낫다
전체 리스트에서 무조건 기본 동작을 막는다 페이지 스크롤이 답답해진다 실제 정렬 중일 때만 필요한 범위에서 막는 편이 좋다
기본 동작을 전혀 안 막는다 순서 변경보다 스크롤이 먼저 반응할 수 있다 핸들에서 시작한 실제 정렬 흐름 안에서만 제어한다

첫 버전에서는 아래 같은 원칙이 현실적입니다.

  • 핸들에서 시작한 경우만 정렬 모드로 본다
  • 정렬 모드가 아닐 때는 목록 스크롤을 최대한 그대로 둔다
  • 정렬 중인 touchmove에서만 필요한 제어를 한다

CSS 쪽에서 핸들에만 별도 힌트를 주는 방식도 도움이 될 수 있습니다.

.drag-handle {

touch-action: none;
}

다만 이 부분은 실제 기기와 브라우저 환경에 따라 체감 차이가 있을 수 있습니다. 그래서 모바일 재정렬은 가능하면 실제 손가락으로 직접 테스트해보는 편이 좋습니다. 데스크톱 개발자 도구의 모바일 흉내만으로는 놓치기 쉬운 느낌 차이가 꽤 있기 때문입니다.

실전 팁
모바일에서 스크롤과 드래그를 모두 만족시키려면, “어디를 누르면 정렬 시작인지”를 먼저 좁히는 편이 “전체를 다 되게 하려는 시도”보다 훨씬 효과적이다.


초보자가 자주 하는 실수 6가지

모바일 재정렬을 붙일 때는 비슷한 실수가 반복됩니다. 겉보기에는 “이벤트 이름만 바꾸면 될 것 같은데” 싶지만, 실제로는 입력 환경이 달라지기 때문에 데스크톱에서 통하던 직관이 그대로 안 맞는 경우가 많습니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
데스크톱의 dragstart, dragover, drop만 그대로 믿는다 모바일에서는 기대한 반응이 안 나온다 터치 환경에서 필요한 상태 추적이 빠졌다 터치용 흐름을 따로 설계하는 편이 낫다
항목 전체를 드래그 시작 영역으로 둔다 스크롤하려다 자꾸 재정렬이 시작된다 스크롤과 정렬의 시작점이 분리되지 않았다 핸들 영역을 따로 두는 편이 훨씬 안정적이다
touchmove에서 현재 대상 요소를 다시 안 찾는다 드롭 위치가 계속 이상하거나 이전 요소 기준으로 남는다 손가락 이동 중 target 추적이 빠졌다 현재 좌표 아래 요소를 다시 찾는 흐름이 필요하다
기본 동작을 전체에서 다 막는다 목록 스크롤이 지나치게 불편해진다 정렬 중이 아닌 상황까지 같이 막았다 정렬이 실제로 진행 중인 범위에서만 제어하는 편이 낫다
touchend만 보고 touchcancel을 빼먹는다 중간에 상태가 꼬여 다음 터치에서 이상하게 이어진다 예상 밖 종료 상황에서 임시 상태를 정리하지 않았다 정리 함수는 종료 계열 이벤트에서 같이 호출하는 편이 좋다
검색, 필터, 자동 정렬 상태에서 그대로 재정렬을 허용한다 결과 설명이 더 어려워지고 버그처럼 느껴진다 화면 계산 규칙과 사용자 순서 변경이 겹쳐 있다 첫 버전은 custom 모드와 전체 보기에서 먼저 안정화한다

특히 다섯 번째 실수는 데스크톱만 테스트하다 보면 의외로 놓치기 쉽습니다. 모바일에서는 중간에 전화 알림이 뜨거나 앱 전환처럼 흐름이 갑자기 끊길 수 있습니다. 이럴 때 임시 정렬 상태를 제대로 정리하지 않으면, 다음 터치부터는 “왜 갑자기 이전 항목이 드래그된 것처럼 보이지?” 같은 묘한 문제가 생길 수 있습니다.

실전에서 가장 많이 흔들리는 부분
모바일 재정렬은 코드 한 줄보다 입력 규칙을 먼저 분명히 해야 한다. 정렬 시작점, 스크롤 허용 범위, 임시 상태 정리 시점이 흐려지면 기능이 금방 불안해진다.


마무리

모바일 재정렬은 데스크톱 드래그를 그대로 옮기는 문제가 아니라, 손가락이라는 전혀 다른 입력 환경에서 무엇을 스크롤로 보고 무엇을 순서 변경으로 볼지 다시 정하는 작업에 가깝습니다. 이 감각이 생기면 왜 핸들이 필요해지는지, 왜 touchmove에서 target을 다시 찾아야 하는지, 왜 전체를 한 번에 다 되게 하려 하기보다 규칙을 줄여서 시작하는 편이 좋은지도 훨씬 또렷해집니다.

 

이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.

첫째, 모바일에서는 같은 손동작이 스크롤과 드래그 후보가 되기 때문에 시작 규칙이 특히 중요하다는 점.

둘째, 첫 버전은 항목 전체가 아니라 핸들에서만 정렬을 시작하게 만드는 편이 훨씬 안정적이라는 점.

셋째, 터치 재정렬도 결국은 임시 상태 추적과 실제 데이터 변경을 분리해서 보는 흐름이라는 점입니다.

 

여기까지 오면 이제 데스크톱용과 모바일용 흐름을 각각 이해한 셈입니다. 다음 편에서는 이 둘을 따로따로 관리하던 구조를 한 단계 더 정리해서, 마우스와 터치를 하나의 재정렬 로직으로 합쳐보는 방법을 다루겠습니다. 즉, 입력 방식은 달라도 최종적으로 바뀌는 것은 같은 order 데이터라는 관점으로 다시 묶어보는 단계입니다.