위로 이동, 아래로 이동 버튼까지 붙인 체크리스트 앱은 이제 제법 손에 잡히는 도구처럼 보입니다. 그런데 여기까지 오면 거의 자연스럽게 다음 욕심이 생깁니다. 버튼 대신 마우스로 끌어서 순서를 바꾸고 싶다는 생각입니다. 화면만 놓고 보면 이게 더 직관적으로 느껴집니다. 손으로 잡아서 원하는 자리에 놓으면 되니까요.

문제는 여기서부터입니다. 드래그 앤 드롭은 겉보기에는 더 자연스럽지만, 코드로 옮기면 오히려 버튼 방식보다 생각할 것이 늘어납니다. 어떤 항목을 잡았는지, 어디 위에 올려놨는지, drop이 왜 안 되는지, 마우스를 놓은 뒤에는 어떤 상태를 비워야 하는지까지 한꺼번에 붙기 때문입니다. 그래서 초보자 입장에서는 “버튼보다 더 쉬울 줄 알았는데 왜 갑자기 더 복잡하지?”라는 느낌을 받기 쉽습니다.

이번 글에서는 드래그 앤 드롭을 너무 거창하게 보지 않고, 결국 어떤 항목을 잡아서 어떤 항목 자리와 바꾸는가라는 데이터 문제로 다시 풀어보겠습니다. 처음부터 완벽한 드래그 정렬을 만들기보다, 어떤 이벤트가 왜 필요한지, 첫 버전은 어디까지 단순하게 만드는 게 좋은지, 그리고 왜 첫 버전에서는 “끼워 넣기”보다 “자리 바꾸기”가 더 이해하기 쉬운지 차근차근 정리해보겠습니다.

이번 글에서 먼저 잡아둘 핵심
겉으로 보이는 문제 실제로 일어나는 일 가장 먼저 봐야 할 방향
마우스로 옮겼는데 새로고침하면 원래대로다 화면만 잠깐 바뀌었고 order 값은 저장되지 않았다 순서 변경이 데이터에서 일어났는지 먼저 본다
드롭이 아예 안 된다 dragover 단계에서 drop을 허용하는 처리가 빠졌을 가능성이 크다 이벤트 흐름이 연결됐는지 본다
다른 항목 위에 놓았는데 엉뚱한 항목이 움직인다 보이는 위치를 믿고 있고, 실제 대상은 id로 찾지 않았다 잡은 항목과 대상 항목을 둘 다 id로 추적하는지 확인한다
정렬 모드가 섞이면서 더 헷갈린다 자동 정렬과 사용자 순서 변경을 동시에 허용했다 첫 버전은 custom 모드에서만 드래그를 허용하는 편이 낫다

이번 글의 핵심 한 줄
드래그 앤 드롭은 마우스 동작처럼 보이지만, 결국은 어떤 항목의 order를 어떻게 바꿀지 정하는 데이터 문제다.


왜 드래그 앤 드롭은 버튼 이동보다 어렵게 느껴질까

이전 글에서 다룬 위로 이동, 아래로 이동 버튼은 의외로 구조가 단순합니다. 사용자가 무엇을 원하는지가 이미 분명하기 때문입니다. 어떤 항목을 위로 한 칸 올릴지, 아래로 한 칸 내릴지 버튼 자체가 방향까지 말해주고 있습니다. 즉, 코드 입장에서는 대상 항목 하나와 방향 하나만 알면 됩니다.

반면 드래그 앤 드롭은 조금 다릅니다. 사용자가 마우스로 움직이는 과정 전체가 하나의 흐름이 됩니다. 어떤 항목을 잡았는지, 어디 위를 지나가는지, 어느 항목 위에 놓았는지, 놓은 뒤 상태를 비웠는지까지 이어져야 합니다. 그래서 버튼 방식보다 더 자연스럽게 보이지만, 코드로 보면 한 단계가 아니라 여러 단계가 연결된 기능에 가깝습니다.

버튼 이동과 드래그 앤 드롭의 차이
방식 초보자 눈에는 코드에서는 실제로
위로/아래로 버튼 조금 투박해 보인다 대상 항목과 방향만 알면 된다
드래그 앤 드롭 훨씬 직관적으로 보인다 잡기, 지나가기, 놓기, 상태 정리까지 단계가 늘어난다

여기서 중요한 건 드래그 앤 드롭이 “더 고급 기능”이라서 어려운 게 아니라는 점입니다. 오히려 사용자 행동이 더 길어졌기 때문에 상태를 더 많이 추적해야 한다는 쪽에 가깝습니다. 그래서 처음에는 화면상 느낌보다 데이터 흐름을 먼저 보는 편이 훨씬 이해가 잘 됩니다.

핵심 포인트
버튼 이동은 “한 칸 위”처럼 방향이 이미 정해져 있지만, 드래그 앤 드롭은 “무엇을 잡았고 어디에 놓았는가”를 따로 기록해야 한다.


드래그 앤 드롭도 결국 데이터 문제다

드래그 앤 드롭을 처음 배울 때 가장 도움 되는 관점은 이것입니다. 마우스 기능처럼 보이지만, 결국 바뀌는 건 order 값이다. 즉, 브라우저 안에서 벌어지는 움직임을 전부 따라가려 하기보다, 마지막에 어떤 데이터 변화가 일어나야 하는지만 먼저 보는 편이 좋습니다.

가장 단순한 첫 버전에서는 이렇게 생각하면 충분합니다.

  1. 내가 어떤 항목을 잡았는지 기억한다.
  2. 어느 항목 위에 놓았는지 기억한다.
  3. 잡은 항목과 대상 항목의 order 값을 바꾼다.
  4. 저장하고 다시 그린다.

이 흐름은 “끼워 넣기”보다 단순합니다. 사용자가 A를 끌어서 B 위에 놓으면, 첫 버전에서는 A와 B의 자리를 바꾸는 방식으로 처리하면 됩니다. 실제 제품에서는 더 자연스럽게 중간에 끼워 넣는 동작을 만들 수도 있지만, 입문자에게는 일단 drag와 drop 이벤트가 데이터 변경으로 이어지는 흐름을 이해하는 편이 훨씬 중요합니다.

첫 버전에서 단순하게 생각할 수 있는 흐름
단계 무엇을 기억하나 왜 필요한가
잡을 때 draggedId 누가 움직이는지 알아야 한다
놓을 때 targetId 누구 자리와 바꿀지 알아야 한다
데이터 변경 두 항목의 order 값 실제 순서 변화가 저장 대상이 되기 때문이다
마무리 draggedId 초기화 다음 드래그에서 이전 상태가 남지 않게 한다

이 지점이 잡히면 드래그 앤 드롭도 갑자기 덜 신비롭게 느껴집니다. 결국은 “잡은 항목 id”와 “대상 항목 id”를 알아낸 뒤, order를 바꾸는 기능으로 다시 설명할 수 있기 때문입니다.

쉽게 말하면
드래그 앤 드롭은 손으로 움직이는 것처럼 보여도, 안쪽에서는 id 두 개와 order 변경 하나로 정리할 수 있다.


꼭 알아야 할 이벤트 4개

드래그 앤 드롭이 갑자기 어려워 보이는 이유 중 하나는 이벤트 이름이 한꺼번에 등장하기 때문입니다. 그런데 초보자가 첫 버전에서 꼭 이해하면 좋은 것은 많지 않습니다. 실제로는 네 가지 정도만 잡아도 흐름이 크게 보입니다.

첫 버전에서 먼저 이해하면 좋은 이벤트 4개
이벤트 언제 일어나나 첫 버전에서 하는 일
dragstart 사용자가 항목을 잡기 시작할 때 draggedId를 기억한다
dragover 다른 항목 위를 지나갈 때 drop을 허용하는 처리를 한다
drop 항목 위에 마우스를 놓을 때 targetId를 기준으로 순서를 바꾼다
dragend 드래그가 끝났을 때 draggedId 같은 임시 상태를 정리한다

여기서 특히 많이 놓치는 것이 dragover입니다. 처음 보는 사람 입장에서는 그냥 지나가는 이벤트처럼 보입니다. 그런데 이 단계에서 drop을 허용하는 처리가 없으면, 드롭이 아예 일어나지 않는 경우가 많습니다. 그래서 drop이 안 될 때는 drop 함수부터 의심하기보다 dragover에서 필요한 처리가 들어갔는지 먼저 보는 편이 빠릅니다.

첫 버전에서는 아래 정도 흐름이면 충분합니다.

let draggedId = null;

function handleDragStart(event, id) {
draggedId = id;
event.dataTransfer.setData('text/plain', String(id));
event.dataTransfer.effectAllowed = 'move';
}

function handleDragOver(event) {
event.preventDefault();
}

function handleDrop(event, targetId) {
event.preventDefault();

if (draggedId === null) return;
if (draggedId === targetId) return;

swapTodoOrder(draggedId, targetId);
draggedId = null;
}

function handleDragEnd() {
draggedId = null;
}

이 코드를 처음 볼 때는 event 객체나 dataTransfer가 눈에 걸릴 수 있습니다. 그런데 지금 단계에서는 깊게 파고들 필요는 없습니다. dragstart는 누구를 잡았는지 기억하는 단계, dragover는 drop을 허용하는 단계, drop은 실제 데이터 변경 단계, dragend는 마무리 단계 정도로 받아들이면 충분합니다.

첫 이해는 이 정도면 충분하다
드래그 이벤트는 어려운 문법이 아니라, 시작-지나감-놓기-정리라는 순서를 가진 흐름이라고 생각하는 편이 훨씬 덜 복잡하다.


가장 단순한 첫 버전: 자리 바꾸기부터 시작하기

드래그 앤 드롭을 처음 붙일 때 많은 사람이 곧바로 “대상 사이에 끼워 넣기”를 떠올립니다. 실제 서비스에서는 그 방식이 더 자연스러울 때가 많습니다. 하지만 초보자 기준에서는 이 단계가 अचानक 복잡해집니다. 잡은 항목이 위에서 내려왔는지 아래에서 올라왔는지, 어디 앞에 넣을지, 어디 뒤에 넣을지까지 계산이 붙기 때문입니다.

그래서 첫 버전에서는 좀 더 단순하게, 잡은 항목과 놓은 항목의 order 값을 서로 바꾸는 방식으로 시작하는 편이 이해하기 쉽습니다. 이 방식은 드롭 동작이 약간 투박해 보여도, 드래그 이벤트가 실제 데이터 변경으로 이어지는 흐름을 잡기에는 충분합니다.

예를 들어 순서 바꾸기 함수는 이렇게 둘 수 있습니다.

function swapTodoOrder(firstId, secondId) {

const first = todos.find(todo => todo.id === firstId);
const second = todos.find(todo => todo.id === secondId);

if (!first || !second) return;

const temp = first.order;
first.order = second.order;
second.order = temp;

saveTodos();
renderTodos();
}

그리고 렌더링할 때 각 항목에 드래그 관련 이벤트를 연결합니다.

function renderTodos() {

const visibleTodos = getVisibleTodos();

todoList.innerHTML = '';

visibleTodos.forEach(todo => {
const li = document.createElement('li');
li.draggable = true;

li.textContent = todo.text;

li.addEventListener('dragstart', (event) => {
  handleDragStart(event, todo.id);
});

li.addEventListener('dragover', (event) => {
  handleDragOver(event);
});

li.addEventListener('drop', (event) => {
  handleDrop(event, todo.id);
});

li.addEventListener('dragend', () => {
  handleDragEnd();
});

todoList.appendChild(li);

});
}

이 구조에서 실제로 바뀌는 것은 두 가지뿐입니다. 하나는 draggedId라는 임시 상태이고, 다른 하나는 항목 두 개의 order 값입니다. 즉, 화면상으로는 마우스로 끌어 옮기는 복잡한 기능처럼 보여도, 데이터 입장에서는 여전히 누구와 누구의 순서를 바꿀까라는 문제로 정리됩니다.

첫 버전에서 정말 필요한 데이터 흐름
단계 코드에서 하는 일 왜 충분한가
잡기 draggedId 저장 현재 움직이는 항목을 잊지 않게 한다
놓기 targetId 확인 어느 자리와 관계를 맺을지 정해진다
순서 변경 두 항목의 order 교환 직관적이고 디버깅이 쉽다
마무리 저장하고 렌더링하고 상태 초기화 다음 실행과 다음 드래그까지 안정적으로 이어진다

초보자에게 특히 좋은 이유
첫 버전에서 자리 바꾸기 방식으로 시작하면, 드래그 이벤트와 order 저장을 먼저 이해할 수 있고, 그다음에야 더 자연스러운 끼워 넣기 방식으로 넘어갈 수 있다.


첫 버전에서는 어디까지 만드는 게 좋을까

드래그 앤 드롭은 욕심을 내기 시작하면 아주 빠르게 복잡해집니다. 시각적인 강조, 위아래 위치에 따른 삽입선, 자동 스크롤, 모바일 터치 대응, 검색 상태에서의 부분 목록 재정렬까지 붙이기 시작하면 초보자가 한 번에 통제하기 어려워집니다. 그래서 첫 버전에서는 일부러 범위를 줄이는 편이 좋습니다.

처음 붙일 때는 아래 정도로 선을 그어두면 훨씬 덜 흔들립니다.

  • custom 모드에서만 드래그 허용
    최신순이나 오래된순 같은 자동 정렬 모드에서는 드래그를 비활성화하는 편이 낫습니다.
  • 전체 보기에서만 재정렬 허용
    검색이나 필터가 걸린 상태에서는 숨겨진 항목 때문에 결과가 더 헷갈릴 수 있습니다.
  • 자리 바꾸기 방식부터 시작
    첫 버전은 swap이 훨씬 디버깅하기 쉽습니다.
  • 데스크톱 기준으로 먼저 이해
    모바일 터치 환경은 동작 방식이 달라질 수 있어, 초반에는 별도 단계로 보는 편이 좋습니다.
첫 버전에서 넣는 편이 좋은 것과 미루는 편이 좋은 것
첫 버전에 넣는 편이 좋은 것 다음 단계로 미루는 편이 좋은 것
dragstart, dragover, drop, dragend 흐름 이해 삽입선 표시, hover 강조 같은 세밀한 UI
항목 두 개의 order 바꾸기 중간 위치 끼워 넣기와 index 보정
저장 후 새로고침해도 순서 유지 검색 결과 화면에서 부분 재정렬 허용
custom 모드 한정 사용 자동 정렬 모드와의 완전한 통합

이렇게 범위를 줄이면 좋은 점이 분명합니다. 문제가 생겼을 때 원인을 훨씬 빨리 좁힐 수 있습니다. 드롭이 안 되면 이벤트 흐름을 보면 되고, 순서가 안 남으면 order 저장을 보면 되고, 보기는 바뀌는데 다시 열면 풀리면 save와 load를 보면 됩니다. 반대로 처음부터 모든 حالت을 한 번에 허용하면 어디서부터 꼬였는지 감이 잘 안 잡힙니다.

입문자에게 가장 현실적인 전략
드래그 앤 드롭은 멋있게 보이는 것보다 먼저, custom 모드에서 순서가 실제로 저장되고 다시 열어도 유지되는지부터 확인하는 편이 훨씬 낫다.


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

드래그 앤 드롭을 붙일 때는 비슷한 실수가 반복됩니다. 겉으로 보면 작은 차이 같지만, 실제로는 대부분 데이터와 이벤트의 경계를 섞는 순간부터 시작됩니다.

실수 겉으로 보이는 증상 실제로 문제인 부분 어떻게 보는 게 좋은가
dragover 처리를 빼먹는다 drop이 전혀 동작하지 않는다 drop 허용 단계가 빠졌다 드롭 문제는 dragover부터 확인하는 편이 빠르다
드래그된 항목 id를 기억하지 않는다 놓았을 때 누가 움직여야 하는지 모른다 draggedId 같은 상태가 없다 드래그 시작 시 누가 움직이는지 반드시 저장해둔다
DOM 위치만 바꾸고 데이터를 안 바꾼다 그 순간엔 움직이지만 새로고침하면 원래대로다 order 값 저장이 빠졌다 화면보다 먼저 데이터에서 순서를 바꾼다
drop 후 상태를 초기화하지 않는다 다음 드래그에서 이전 항목 정보가 남아 이상하게 움직인다 draggedId 정리가 빠졌다 drop과 dragend 모두에서 상태 정리를 의식한다
자동 정렬 모드에서도 그대로 재정렬을 허용한다 움직인 것 같다가도 화면이 다시 원래 규칙대로 보인다 createdAt 같은 자동 정렬 규칙이 화면을 다시 덮어쓴다 첫 버전은 custom 모드에서만 허용하는 편이 낫다
모바일까지 처음부터 한 번에 맞추려 한다 데스크톱도 불안정한데 전체가 더 복잡해진다 입력 방식이 다른 환경을 동시에 해결하려 했다 처음에는 데스크톱 기준으로 흐름을 먼저 안정화한다

특히 세 번째 실수는 정말 자주 보입니다. 사용자가 드래그하니까, 초보자는 화면에서 항목을 옮기는 것 자체가 기능의 핵심이라고 느끼기 쉽습니다. 그런데 실제 앱 입장에서는 그것만으로는 아무 일도 끝나지 않습니다. order 값이 바뀌고 저장돼야 그게 진짜 순서 변경입니다. 이 차이를 이해하는 순간 드래그 앤 드롭도 훨씬 덜 막막해집니다.

실전에서 가장 많이 흔들리는 부분
드래그 앤 드롭은 눈으로 보기에는 화면 기능 같지만, 실제로는 이벤트와 order 저장이 연결되지 않으면 완성된 기능이라고 보기 어렵다.


마무리

드래그 앤 드롭은 입문자에게 꽤 매력적인 기능입니다. 눈에 잘 보이고, “내가 앱을 직접 만지고 있다”는 느낌도 강하기 때문입니다. 하지만 그만큼 착각하기 쉬운 기능이기도 합니다. 마우스로 움직이는 동작이 전부인 것처럼 보이지만, 실제로는 잡은 항목을 기억하고, 놓인 대상을 확인하고, order 값을 바꾸고, 저장하고, 다시 그리는 흐름이 모두 맞물려야 합니다.

 

이번 글에서 꼭 가져가면 좋은 건 세 가지입니다.
첫째, 드래그 앤 드롭은 결국 id와 order를 다루는 데이터 문제라는 점.
둘째, 첫 버전에서는 끼워 넣기보다 자리 바꾸기 방식이 훨씬 이해하기 쉽다는 점.
셋째, custom 모드와 전체 보기처럼 조건을 일부러 줄여서 시작하는 편이 초보자에게 훨씬 안정적이라는 점입니다.

 

여기까지 오면 이제 드래그 이벤트 자체는 더 이상 낯선 덩어리가 아닙니다. 다음 편에서는 자리 바꾸기에서 한 단계 더 나아가, 항목을 자연스럽게 사이에 끼워 넣는 방식이 왜 더 어렵고, index 보정이라는 개념이 왜 여기서부터 중요해지는지를 다룹니다.