웹 애플리케이션을 개발하면서 마우스로 요소를 드래그하여 순서를 바꾸거나 파일을 업로드하는 드래그 앤 드롭(Drag and Drop) 기능을 한번쯤 구현해봤을 것이다.
다만 대부분의 개발자들은 dragover 이벤트에서 왜 preventDefault()를 호출해야 하는지, 혹은 드롭 존(Drop Zone)에 진입할 때 자식 요소 때문에 이벤트가 튀는 현상을 정확히 어떻게 해결해야 하는지 모른 채 외부 라이브러리에만 의존하거나 인터넷의 파편화된 코드를 복사해 붙인다.
이번 글에서는 HTML5 순수 JavaScript Drag and Drop API의 동작 원리부터, 실무에서 빈번하게 발생하는 드롭 불가능 문제 해결법, 그리고 리스트 순서를 자유롭게 변경하는 실전 예제까지 완벽하게 정리해서 소개하겠다.

 

1. Drag and Drop API의 기본 원리와 이벤트 흐름

HTML5의 Drag and Drop API는 별도의 외부 라이브러리 없이 브라우저 자체 기능만으로 요소를 이동시키거나 데이터를 전달할 수 있게 해준다.
드래그 앤 드롭이 동작하려면 먼저 드래그할 대상 요소에 draggable="true" 속성을 부여해야 한다. 이후 드래그 요소(Drag Source)와 드롭 요소(Drop Target) 사이에서 발생하는 일련의 이벤트를 제어하게 된다.

 

주요 Drag & Drop 이벤트 비교
이벤트명발생 대상설명
dragstart드래그 요소사용자가 요소를 드래그하기 시작할 때 1회 발생
drag드래그 요소요소를 드래그하는 동안 지속적으로 발생
dragend드래그 요소드래그 동작이 종료될 때(마우스 버튼을 뗄 때) 발생
dragenter드롭 대상드래그한 요소가 드롭 영역 안으로 들어올 때 발생
dragover드롭 대상드래그한 요소가 드롭 영역 위에 머물러 있을 때 지속적으로 발생
dragleave드롭 대상드래그한 요소가 드롭 영역 밖으로 벗어날 때 발생
drop드롭 대상드래그한 요소를 드롭 영역에 놓았을 때 발생

 

2. 흔히 겪는 실수: drop 이벤트가 작동하지 않는 이유

드래그 앤 드롭을 처음 구현할 때 가장 많이 겪는 문제는 drop 이벤트를 등록했음에도 마우스를 놓았을 때 아무런 반응이 없는 현상이다.
웹 브라우저는 기본적으로 대부분의 영역에 요소를 드롭하는 것을 거부(Default Prevented)하도록 설정되어 있다. 따라서 드롭을 허용하려면 dragover 이벤트에서 반드시 e.preventDefault()를 호출하여 브라우저의 기본 동작을 막아야 한다.

또한 dragenterdragleave를 통해 드롭 존의 스타일(예: 테두리 하이라이트)을 변경할 때, 드롭 존 내부의 자식 요소를 지나치면 이벤트가 번갈아 호출되어 스타일이 깜빡이는 문제가 생긴다. 이는 CSS의 pointer-events: none; 속성을 자식 요소에 부여하거나 이벤트 버블링을 제어하여 해결할 수 있다.

 

3. 실전 예제: 드래그 앤 드롭을 이용한 리스트 순서 변경

다음은 HTML5 Drag and Drop API를 사용하여 목록 항목(Li)의 순서를 동적으로 변경하는 실무 예제 코드다.

 

✗ 잘못된 구현 방식 (e.preventDefault 미호출)

✗ dragover에서 기본 동작을 차단하지 않아 drop 이벤트가 실행되지 않으며, 드래그 중인 요소를 추적하지 못하는 코드다.

// 잘못된 예시: dragover 처리가 누락되어 drop이 동작하지 않음
const listItems = document.querySelectorAll('.drag-item');

listItems.forEach(item => {
  item.addEventListener('dragstart', (e) => {
    console.log('드래그 시작');
  });

  // dragover에서 e.preventDefault()가 없으면 브라우저가 드롭을 허용하지 않음!
  item.addEventListener('dragover', (e) => {
    // 아무 처리도 하지 않음
  });

  item.addEventListener('drop', (e) => {
    console.log('드롭 성공'); // 이 코드는 절대 실행되지 않는다.
  });
});

 

✓ 올바른 구현 방식 (순서 변경 및 스타일 최적화)

✓ dragover에서 preventDefault()를 호출하고, 현재 마우스 위치에 따라 위치를 유동적으로 바꿔주는 올바른 실무 코드다.

const container = document.getElementById('drag-list');
let draggingItem = null;

// 드래그 시작 이벤트
container.addEventListener('dragstart', (e) => {
  if (!e.target.classList.contains('drag-item')) return;
  draggingItem = e.target;
  e.target.classList.add('dragging');
  e.dataTransfer.effectAllowed = 'move';
});

// 드래그 종료 이벤트
container.addEventListener('dragend', (e) => {
  if (!e.target.classList.contains('drag-item')) return;
  e.target.classList.remove('dragging');
  draggingItem = null;
});

// 드롭 영역 위에서 드래그 중일 때 (위치 계산)
container.addEventListener('dragover', (e) => {
  e.preventDefault(); // 필수: 브라우저 기본 드롭 방지 해제

  const afterElement = getDragAfterElement(container, e.clientY);
  if (afterElement == null) {
    container.appendChild(draggingItem);
  } else {
    container.insertBefore(draggingItem, afterElement);
  }
});

// 마우스 Y 좌표를 기준으로 가장 가까운 하위 요소 찾기 함수
function getDragAfterElement(container, y) {
  const draggableElements = [...container.querySelectorAll('.drag-item:not(.dragging)')];

  return draggableElements.reduce((closest, child) => {
    const box = child.getBoundingClientRect();
    const offset = y - box.top - box.height / 2;

    if (offset < 0 && offset > closest.offset) {
      return { offset: offset, element: child };
    } else {
      return closest;
    }
  }, { offset: Number.NEGATIVE_INFINITY }).element;
}

 

실행 결과 및 동작 설명

위 코드 적용 후 요소를 마우스로 클릭하여 드래그하면 dragging 클래스가 부여되며 반투명하게 변경된다. 마우스를 다른 리스트 항목 위로 이동하면 getDragAfterElement 함수가 Y축 좌표를 계산하여 드래그 중인 요소가 들어갈 위치를 실시간으로 탐색하고, insertBefore를 통해 DOM 순서를 자연스럽게 교체한다.

 

4. 실무 주의사항 및 흔한 실수

1. DataTransfer 객체 사용 시 주의사항: e.dataTransfer.setData('text/plain', value)로 데이터를 전달할 수 있지만, dragover 이벤트 내에서는 보안상의 이유로 getData()를 읽을 수 없다. 데이터 읽기는 반드시 drop 이벤트에서 수행해야 한다.
2. 모바일 브라우저 미지원: HTML5 Drag and Drop API는 데스크톱 브라우저 전용에 가깝다. iOS Safari나 Android Chrome 등의 터치 기반 모바일 기기에서는 touchstart, touchmove, touchend 이벤트를 사용하여 별도로 구현하거나 Polyfill 라이브러리를 사용해야 한다.
3. 자식 요소 이벤트 간섭: 드롭 영역 내부에 텍스트나 이미지가 있을 경우 드래그 이벤트가 자식 요소에서 산발적으로 발생하는 문제가 있다. CSS로 드롭 영역 내부 요소에 pointer-events: none;을 설정하면 깜빡임 없이 안정적인 드롭 이벤트를 수신할 수 있다.

 

5. 마무리

JavaScript Drag and Drop API는 브라우저의 이벤트 메커니즘만 정확히 이해하면 외부 라이브러리 없이도 매끄럽고 빠른 인터랙션 UI를 제작할 수 있는 강력한 기능이다. 필수적인 e.preventDefault() 처리와 위치 계산 로직이라는 작은 최적화가 모여서 웹 서비스의 사용자 경험(UX)을 극대화한다는 점을 잊지 말자. 이 글의 실전 예제 코드를 참고해 프로젝트의 리스트 관리 기능에 직접 적용해 보면, 더욱 직관적인 UI를 구현할 수 있을 것이다.