데스크톱용 드래그와 모바일용 터치 재정렬을 각각 붙이고 나면, 처음에는 꽤 뿌듯합니다. 한쪽에서는 마우스로 움직이고, 다른 쪽에서는 손가락으로 순서를 바꿀 수 있으니까요. 그런데 조금만 지나면 또 다른 문제가 보입니다. 같은 기능을 고치는데 코드가 두 군데에 있고, 드롭 위치 규칙을 바꾸려면 마우스 쪽도 수정하고 터치 쪽도 수정해야 하고, 어느 한쪽에서만 버그를 고쳤더니 다른 쪽은 그대로 남아 있는 식입니다.
이 단계에서 많은 입문자가 두 가지 중 하나로 갑니다. 하나는 그냥 두 벌의 코드를 계속 따로 유지하는 것이고, 다른 하나는 마우스와 터치를 한 개의 거대한 이벤트 함수로 억지로 합치려는 것입니다. 그런데 둘 다 오래 가기는 쉽지 않습니다. 전자는 같은 로직을 계속 두 번 고쳐야 하고, 후자는 이벤트 모양이 다른 입력을 한 함수 안에 다 집어넣으려다 코드가 급격히 무거워집니다.
여기서 필요한 건 “이벤트를 하나로 합치기”가 아니라, 실제로 공통인 것만 공통 로직으로 묶는 일입니다. 입력 방식은 달라도, 최종적으로 바뀌는 건 결국 같은 데이터입니다. dragged 항목이 누구인지, target이 누구인지, 앞에 넣을지 뒤에 넣을지, 그리고 그 결과로 order를 어떻게 바꿀지. 이 핵심이 같다면 그 부분은 하나의 재정렬 로직으로 묶는 편이 훨씬 안정적입니다.
이번 글에서는 왜 드래그 코드가 두 벌이 되면 금방 흔들리는지, 무엇을 공통으로 묶고 무엇은 입력 방식별로 남겨둬야 하는지, 그리고 초보자 기준에서 가장 현실적인 재정렬 구조는 어떤 모습인지 차근차근 정리해보겠습니다.
| 겉으로 보이는 문제 | 실제로 일어나는 일 | 가장 먼저 봐야 할 방향 |
|---|---|---|
| 같은 버그를 마우스 쪽과 터치 쪽에서 각각 고치고 있다 | 실제 데이터 재배치 로직이 입력별 코드 안에 중복돼 있다 | 입력별 처리와 공통 재배치 로직을 분리한다 |
| 한쪽만 수정하면 다른 쪽 동작이 달라진다 | before, after 규칙이나 order 재계산이 두 군데에 따로 있다 | 최종 재배치 규칙은 하나의 함수에서만 맡긴다 |
| 이벤트를 하나로 합치려다 코드가 더 복잡해진다 | 입력 방식이 다른 raw event를 한 함수가 다 이해하려고 한다 | 이벤트는 따로 받고, 공통 데이터만 아래로 넘긴다 |
| 구조를 조금만 바꿔도 전체가 흔들린다 | 이벤트 처리, 상태 추적, 데이터 변경, 저장, 렌더링이 한 덩어리다 | 역할별 경계를 먼저 세운다 |
이번 글의 핵심 한 줄
마우스와 터치는 입력 방식이 다를 뿐이고, 최종적으로 바뀌는 데이터는 같다. 그래서 이벤트는 나뉘어도 재정렬 핵심 로직은 하나로 묶는 편이 안정적이다.
목차
- 왜 드래그 로직이 두 벌이 되면 금방 흔들릴까
- 공통으로 묶을 것과 따로 둘 것을 먼저 나눠야 한다
- 입력은 달라도 결국 필요한 정보는 같다
- 공통 재정렬 함수는 여기까지 맡기면 된다
- 마우스와 터치 핸들러는 얇게 두는 편이 좋은 이유
- 초보자가 자주 하는 실수 6가지
- 마무리
왜 드래그 로직이 두 벌이 되면 금방 흔들릴까
처음에는 분리해 두는 편이 오히려 편해 보일 수 있습니다. 마우스는 마우스대로, 터치는 터치대로 흐름이 다르기 때문입니다. 실제로 시작 이벤트도 다르고, 움직이는 동안 현재 위치를 읽는 방식도 다릅니다. 그래서 처음에는 “그냥 따로 짜는 게 더 단순하지 않나?”라는 생각이 들기 쉽습니다.
문제는 기능이 자라기 시작할 때입니다. 예를 들어 드롭 규칙을 바꿔서 target 앞이 아니라 뒤에 넣는 조건을 추가했다고 해보겠습니다. 또는 source index와 target index가 같을 때는 아무 일도 안 하게 하기로 했다고 해보겠습니다. 이런 규칙은 입력 방식과 무관하게 최종 재배치 결과에 관한 것입니다. 그런데 이 로직이 마우스 코드와 터치 코드에 각각 들어 있으면, 같은 수정을 두 번 해야 하고 언젠가는 둘 중 하나만 바뀌는 순간이 생깁니다.
| 상태 | 처음엔 왜 편해 보이나 | 나중에 왜 불편해지나 |
|---|---|---|
| 마우스와 터치를 완전히 따로 구현 | 각 입력 방식만 보면 흐름이 짧아 보인다 | 재배치 규칙, 저장, 상태 초기화를 두 번 고쳐야 한다 |
| 모든 걸 한 개의 거대한 이벤트 함수에 몰아넣음 | 겉으로는 하나의 코드로 통일된 것처럼 보인다 | event 형태 차이까지 한 함수가 떠안으면서 오히려 읽기 어려워진다 |
| 입력별 처리와 공통 재배치 로직을 분리 | 처음엔 한 단계 더 나눠 보는 느낌이 든다 | 규칙 변경 지점이 줄고 입력별 버그를 따로 보기 쉬워진다 |
즉, 문제는 입력 방식이 둘이라는 사실 자체가 아닙니다. 같은 데이터를 바꾸는 로직이 두 벌로 퍼져 있다는 점이 더 큰 문제입니다. 이걸 잡아두면 이후에 드롭 규칙을 바꾸거나 order 계산 방식을 손볼 때 훨씬 덜 불안해집니다.
핵심 포인트
입력 방식이 다르다고 해서 최종 데이터 변경 로직까지 따로 가져갈 필요는 없다. 오히려 그 부분이 분리돼 있을수록 같은 버그를 두 번 고치게 된다.
공통으로 묶을 것과 따로 둘 것을 먼저 나눠야 한다
여기서 가장 중요한 감각은 “무조건 하나로 합친다”가 아닙니다. 무엇은 공통으로 묶고, 무엇은 입력별로 남겨둘지 선을 긋는 것입니다. 이 경계가 흐려지면 공통 함수도 무거워지고, 입력별 함수도 여전히 중복이 많아집니다.
입문자 기준에서는 아래 정도로 나눠 보면 가장 이해가 쉽습니다.
| 구분 | 공통으로 묶는 편이 좋은 것 | 입력별로 두는 편이 좋은 것 |
|---|---|---|
| 시작 | 정렬 상태를 시작하는 함수 | 마우스의 시작 이벤트와 터치의 시작 이벤트 자체 |
| 대상 추적 | 현재 target id와 before 또는 after 상태를 저장하는 방식 | 마우스 좌표를 읽는 법, 터치 좌표를 읽는 법 |
| 확정 | order 재배치, 저장, 렌더링 | drop과 touchend가 언제 일어나는지에 대한 처리 |
| 정리 | 임시 정렬 상태를 비우는 함수 | dragend, touchend, touchcancel 같은 종료 이벤트 연결 |
이 표를 보면 핵심이 보입니다. 이벤트 이름과 좌표 읽기 방식은 다르지만, 그 결과로 얻고 싶은 정보는 비슷합니다. dragged 항목이 누구인지, target이 누구인지, 앞에 넣을지 뒤에 넣을지, 실제로 정렬을 확정할지 말지. 그래서 raw event 자체를 억지로 하나로 만들려 들기보다, event에서 필요한 정보만 꺼내서 공통 로직으로 내려보내는 구조가 훨씬 낫습니다.
쉽게 말하면
이벤트는 다르게 들어와도 괜찮다. 중요한 건 아래 단계로 내려갈 때 어떤 정보로 바꿔서 넘기느냐다.
입력은 달라도 결국 필요한 정보는 같다
이제 공통 상태를 조금 더 분명하게 보겠습니다. 데스크톱이든 모바일이든 재정렬을 하려면 결국 아래 같은 정보가 필요합니다.
- 누가 움직이는가
- 지금 누구를 기준으로 보고 있는가
- 그 기준의 앞에 넣을 것인가, 뒤에 넣을 것인가
- 지금 정렬 중인가
이걸 상태 객체로 묶으면 아래처럼 볼 수 있습니다.
let sortState = {
isSorting: false,
draggedId: null,
targetId: null,
position: null
};
여기서 중요한 점은 이 구조가 마우스 전용도, 터치 전용도 아니라는 것입니다. 그냥 재정렬을 설명하는 데 필요한 최소 정보입니다. 마우스는 dragstart나 dragover 같은 이벤트를 통해 이 값을 채워 넣고, 터치는 touchstart나 touchmove를 통해 같은 값을 채워 넣으면 됩니다.
| 이름 | 뜻 | 입력 방식과 무관하게 필요한가 |
|---|---|---|
| isSorting | 지금 정렬 흐름 안에 있는지 | 예 |
| draggedId | 현재 움직이는 항목의 id | 예 |
| targetId | 현재 드롭 기준이 되는 항목의 id | 예 |
| position | before인지 after인지 | 예 |
이 구조가 중요한 이유는, 이제부터는 마우스든 터치든 결국 이 상태를 업데이트하는 역할만 맡기면 된다는 점입니다. 즉, 이벤트 처리 함수는 “무슨 종류의 event가 들어왔는가”보다 이 상태를 어떻게 채울 것인가에 집중하면 됩니다.
핵심 정리
이벤트를 직접 공통화하려고 하기보다, 이벤트가 남겨야 하는 상태를 공통화하는 편이 훨씬 현실적이다.
공통 재정렬 함수는 여기까지 맡기면 된다
이제 핵심입니다. 입력 방식이 무엇이든, 최종적으로 실제 데이터를 바꾸는 구간은 하나로 묶는 편이 좋습니다. 즉, 아래 같은 흐름은 마우스와 터치가 따로 들고 갈 이유가 거의 없습니다.
- draggedId, targetId, position이 충분한지 검사한다.
- 같은 항목 위로 자기 자신을 놓는 상황인지 본다.
- 재정렬 함수를 호출한다.
- 저장한다.
- 렌더링한다.
- 임시 정렬 상태를 비운다.
예를 들면 아래처럼 공통 확정 함수를 둘 수 있습니다.
function commitSorting() {
const { isSorting, draggedId, targetId, position } = sortState;
if (!isSorting) {
clearSortingState();
return;
}
if (draggedId === null || targetId === null || !position) {
clearSortingState();
return;
}
if (draggedId === targetId) {
clearSortingState();
return;
}
moveTodoByDropPosition(draggedId, targetId, position);
clearSortingState();
}
function clearSortingState() {
sortState = {
isSorting: false,
draggedId: null,
targetId: null,
position: null
};
}
이 코드의 장점은 분명합니다. 이제 order를 실제로 어떻게 바꿀지, 저장은 언제 할지, 정렬 종료 후 상태를 어떻게 비울지는 한 군데에서만 관리됩니다. 이후 드롭 규칙을 바꾸거나, 드래그 종료 시 예외 처리를 늘리거나, 저장 타이밍을 조정하고 싶을 때도 이 구간만 보면 됩니다.
| 맡기는 일 | 왜 공통으로 두는 편이 좋은가 |
|---|---|
| 입력 검증 | 마우스와 터치가 같은 기준으로 종료 조건을 공유할 수 있다 |
| 실제 재배치 | before와 after, order 재계산 규칙을 한 곳에서만 관리할 수 있다 |
| 상태 초기화 | 한쪽만 상태를 비우고 다른 쪽은 남는 문제를 줄일 수 있다 |
중요한 건 여기까지입니다. 공통 함수는 raw event를 이해할 필요가 없습니다. clientY가 어디에 있었는지, touch 좌표가 어떻게 들어오는지 같은 건 입력 쪽에서 처리하면 됩니다. 공통 함수는 이미 정리된 상태 정보만 받아서 데이터 변경을 책임지는 편이 가장 깔끔합니다.
가장 중요한 기준
공통 함수는 event를 해석하는 곳이 아니라, 해석이 끝난 결과를 가지고 실제 순서를 바꾸는 곳이어야 한다.
마우스와 터치 핸들러는 얇게 두는 편이 좋은 이유
이제 입력별 함수는 훨씬 가벼워질 수 있습니다. 이 함수들은 더 이상 order 계산이나 저장, 렌더링 전체를 직접 책임질 필요가 없습니다. 대신 “지금 들어온 입력을 sortState 형태로 바꿔주는 역할” 정도만 맡기면 됩니다.
예를 들어 마우스 쪽은 아래처럼 단순해질 수 있습니다.
function handleMouseDragStart(todoId) {
sortState.isSorting = true;
sortState.draggedId = todoId;
}
function handleMouseDragOver(event, todoId, element) {
event.preventDefault();
const position = getDropPositionFromY(event.clientY, element);
sortState.targetId = todoId;
sortState.position = position;
}
function handleMouseDrop(event) {
event.preventDefault();
commitSorting();
}
function handleMouseDragEnd() {
clearSortingState();
}
터치 쪽도 비슷한 역할만 맡게 만들 수 있습니다.
function handleTouchStart(todoId) {
sortState.isSorting = true;
sortState.draggedId = todoId;
}
function handleTouchMove(event) {
if (!sortState.isSorting) 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;
const todoId = Number(item.dataset.todoId);
const position = getDropPositionFromY(touch.clientY, item);
sortState.targetId = todoId;
sortState.position = position;
event.preventDefault();
}
function handleTouchEnd() {
commitSorting();
}
function handleTouchCancel() {
clearSortingState();
}
이 구조가 좋은 이유는 분명합니다. 마우스와 터치가 서로 다른 점은 그대로 인정하면서도, 결국 같은 상태를 채우고 같은 공통 함수로 내려간다는 일관성을 가질 수 있기 때문입니다. 그래서 드롭 규칙이 바뀌면 commitSorting이나 moveTodoByDropPosition만 보면 되고, 좌표 읽는 방식을 바꾸고 싶으면 입력별 함수만 보면 됩니다.
| 장점 | 왜 체감이 큰가 |
|---|---|
| 수정 범위가 줄어든다 | 입력 방식 수정과 재배치 규칙 수정을 분리해서 볼 수 있다 |
| 디버깅이 쉬워진다 | 이벤트 문제인지, 데이터 변경 문제인지 경계가 더 분명해진다 |
| 같은 버그를 두 번 고칠 일이 줄어든다 | 공통 로직이 하나라서 동작 기준도 하나로 맞춰진다 |
여기서 많은 입문자가 오해하는 부분이 있습니다. “공통 로직으로 묶는다”는 말이 입력별 핸들러까지 모두 똑같이 만들라는 뜻은 아닙니다. 오히려 입력별 핸들러는 다르게 두되, 그들이 결국 같은 상태와 같은 확정 함수로 연결되도록 만드는 편이 훨씬 현실적입니다.
핵심 정리
입력별 핸들러는 raw event를 읽는 얇은 껍데기처럼 두고, 실제 데이터 변경은 공통 함수가 맡게 하면 구조가 훨씬 덜 흔들린다.
초보자가 자주 하는 실수 6가지
이 단계에서 자주 나오는 실수는 조금 비슷합니다. 공통화 자체가 문제가 아니라, 어디까지를 공통으로 묶을지 경계가 흐린 경우가 많습니다.
| 실수 | 겉으로 보이는 증상 | 실제로 문제인 부분 | 어떻게 보는 게 좋은가 |
|---|---|---|---|
| 마우스용과 터치용에서 재배치 함수를 각각 따로 갖고 있다 | 버그를 한쪽만 고쳐서 동작이 달라진다 | 최종 데이터 변경 로직이 중복돼 있다 | 실제 재배치와 저장은 하나의 공통 함수가 맡는 편이 낫다 |
| 모든 raw event를 한 함수에서 직접 처리하려 한다 | 조건문이 많아지고 읽기가 갑자기 어려워진다 | 공통화 경계를 event 해석 단계까지 너무 올렸다 | event 해석은 입력별, 데이터 변경은 공통으로 나눈다 |
| sortState를 입력별로 따로 가진다 | 상태 이름은 비슷한데 정리 시점이 자꾸 다르다 | 공통 상태를 굳이 둘로 나눴다 | draggedId, targetId, position은 하나의 구조로 보는 편이 낫다 |
| clearSortingState를 한쪽 종료 흐름에만 붙인다 | 다음 입력에서 이전 상태가 남아 이상하게 이어진다 | 공통 정리 단계가 빠져 있다 | 정리 함수는 공통으로 두고 각 종료 이벤트가 그 함수를 호출하게 만든다 |
| 입력별 규칙과 공통 규칙이 섞여 있다 | 모바일에서만 스크롤 예외가 생기고 데스크톱에도 조건이 퍼진다 | 입력 특수 규칙이 공통 함수 안까지 내려왔다 | 스크롤 제어 같은 입력 특수성은 입력별 핸들러에 남긴다 |
| 공통화한 뒤에도 렌더링과 저장을 여러 곳에서 따로 호출한다 | 수정 지점이 줄지 않고 여전히 여기저기 흩어진다 | 재정렬 완료 시점이 하나로 모이지 않았다 | 확정 함수 안에서만 저장과 렌더링이 일어나게 정리한다 |
여기서 특히 많이 나오는 건 두 번째 실수입니다. 공통화한다고 해서 event.clientY와 touch 좌표 읽기까지 한 함수가 다 하게 만들면, 오히려 코드가 더 무거워집니다. 공통화의 목표는 “모든 줄을 하나로 합치는 것”이 아니라, 같은 의미를 가진 로직을 한곳에 모으는 것입니다. 이 차이를 놓치면 공통화도 실패하고 분리도 실패하게 됩니다.
실전에서 가장 많이 흔들리는 부분
공통화는 이벤트 종류를 하나로 만드는 일이 아니라, 서로 다른 이벤트가 결국 같은 상태와 같은 재배치 규칙으로 모이게 만드는 일에 더 가깝다.
'바이브코딩 > 이론' 카테고리의 다른 글
| 19. 수정 중인 값은 왜 따로 들고 가야 할까: 체크리스트 앱 임시 편집값 정리 (0) | 2026.04.14 |
|---|---|
| 18. 드래그 중에 수정 버튼 누르면 왜 꼬일까: 바이브코딩 상태 충돌 정리 (0) | 2026.04.09 |
| 16. 데스크톱에선 되는데 모바일에선 왜 안 될까: 터치 기반 재정렬 입문 (0) | 2026.04.02 |
| 15. 리스트가 길어지면 왜 드래그가 버벅일까: 바이브코딩에서 렌더링과 이벤트 정리 (0) | 2026.03.31 |
| 14. 드롭은 됐는데 왜 앞뒤가 자꾸 틀릴까: 바이브코딩에서 마우스 위치 읽는 법 (0) | 2026.03.26 |
