마우스 이벤트로 구현하는 드래그 앤 드롭

HTML5 드래그 앤 드롭의 한계

HTML5에서는 dragstart, dragend 등 몇몇 드래그 전용 이벤트를 지원하여 파일 관리나 이미지 이동 같은 기본 기능을 제공하지만, 기본 드래그 이벤트는 몇 가지 제약이 있습니다.

  • 제어 부족: 드래그 방향 제한이나 특정 조건에서의 드래그 차단이 어렵습니다.
  • 모바일 지원 부족: 모바일 환경에서 일관된 동작을 보장하지 않습니다.

이러한 한계를 극복하기 위해, 마우스 이벤트(mousedown, mousemove, mouseup)를 이용하여 보다 세밀하게 제어할 수 있는 드래그 앤 드롭 기능을 직접 구현할 수 있습니다.

마우스 이벤트 기반 드래그 앤 드롭

mousedown 이벤트

드래그할 요소에 대한 초기 설정을 합니다. 이때, 요소의 현재 위치와 포인터의 상대적 오프셋(shift)을 계산해 둡니다.

mousemove 이벤트

포인터가 이동할 때마다 요소의 위치를 업데이트합니다. 이때, 초기 오프셋을 반영하여 포인터가 요소 내부에서 처음 클릭한 위치를 유지하도록 합니다.

mouseup 이벤트

드래그가 종료되면, 이벤트 핸들러를 해제하여 추가적인 위치 업데이트를 중단합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>드래그 앤 드롭: 기본 구현</title>
  <style>
    #dragCircle {
      width: 60px; height: 60px; background: #ff7043; border-radius: 50%;
      position: absolute; top: 50px; left: 50px; text-align: center; line-height: 60px; color: white;
      cursor: move; user-select: none;
    }
  </style>
</head>
<body>
  <div id="dragCircle">드래그</div>
  <script>
    const dragCircle = document.getElementById('dragCircle');

    dragCircle.addEventListener('mousedown', (e) => {
      e.preventDefault(); // 텍스트 선택 방지

      const shiftX = e.clientX - dragCircle.getBoundingClientRect().left;
      const shiftY = e.clientY - dragCircle.getBoundingClientRect().top;

      dragCircle.style.position = 'absolute';
      dragCircle.style.zIndex = 1000;

      const moveAt = (x, y) => {
        dragCircle.style.left = `${x - shiftX}px`;
        dragCircle.style.top = `${y - shiftY}px`;
      };

      moveAt(e.clientX, e.clientY);

      const onMove = (moveEvent) => {
        moveAt(moveEvent.clientX, moveEvent.clientY);
      };

      document.addEventListener('mousemove', onMove);

      document.addEventListener('mouseup', () => {
        document.removeEventListener('mousemove', onMove);
      }, { once: true });
    });

    dragCircle.ondragstart = () => false; // 기본 드래그 비활성화
  </script>
</body>
</html>
  • shiftX, shiftY: 클릭 지점과 요소 좌상단 간의 오프셋을 계산해 드래그 중 일관성 위치 유지하는 변수
  • mouseup: once: true 옵션으로 단일 실행 후 자동 제거

드롭 존 감지 및 상호작용

단순히 요소를 이동하는 것뿐만 아니라, 드래그 중 특정 영역을 감지하고, 드롭 시 특정 동작을 실행해야 합니다. 이를 위해 document.elementFromPoint(clientX, clientY) 메서드를 사용할 수 있습니다. 이 메서드는 화면상의 특정 좌표 아래에 위치한 요소를 반환합니다.

<!DOCTYPE html>
<html lang="ko">
<head>
  <meta charset="UTF-8">
  <title>드래그 앤 드롭: 드롭 존 감지</title>
  <style>
    #dragItem {
      width: 50px; height: 50px; background: #ab47bc; position: absolute;
      top: 20px; left: 20px; color: white; text-align: center; line-height: 50px;
      cursor: move; user-select: none;
    }
    #dropZone {
      width: 200px; height: 200px; border: 2px dashed #555; margin-top: 100px;
      transition: background 0.2s;
    }
    #dropZone.active { background: #e1bee7; }
  </style>
</head>
<body>
  <div id="dragItem">드래그</div>
  <div id="dropZone">드롭 존</div>
  <script>
    const dragItem = document.getElementById('dragItem');
    const dropZone = document.getElementById('dropZone');
    let activeDrop = null;

    dragItem.addEventListener('mousedown', (e) => {
      e.preventDefault();

      const rect = dragItem.getBoundingClientRect();
      const shiftX = e.clientX - rect.left;
      const shiftY = e.clientY - rect.top;

      dragItem.style.position = 'absolute';
      dragItem.style.zIndex = 1000;

      const moveAt = (x, y) => {
        dragItem.style.left = `${x - shiftX}px`;
        dragItem.style.top = `${y - shiftY}px`;
      };

      moveAt(e.clientX, e.clientY);

      const onMove = (moveEvent) => {
        moveAt(moveEvent.clientX, moveEvent.clientY);

        // 드롭 존 감지
        dragItem.style.visibility = 'hidden';
        const below = document.elementFromPoint(moveEvent.clientX, moveEvent.clientY);
        dragItem.style.visibility = 'visible';

        const newDrop = below?.closest('#dropZone');
        if (activeDrop !== newDrop) {
          if (activeDrop) activeDrop.classList.remove('active');
          activeDrop = newDrop;
          if (activeDrop) activeDrop.classList.add('active');
        }
      };

      document.addEventListener('mousemove', onMove);

      document.addEventListener('mouseup', () => {
        document.removeEventListener('mousemove', onMove);
        if (activeDrop) {
          console.log('드롭 완료: dropZone에 놓임');
          activeDrop.classList.remove('active');
          activeDrop = null;
        }
      }, { once: true });
    });

    dragItem.ondragstart = () => false;
  </script>
</body>
</html>
  • elementFromPoint: 드래그 중 요소를 visibility로 숨기고 아래 드롭 존 감지
  • activeDrop: 활성화 드랍 존 요소 기억