4P by GN⁺ | ★ favorite | 댓글 1개
  • Red Blob Games의 인터랙티브 다이어그램은 독자가 객체를 직접 움직이면 화면이 즉시 반응해야 하므로, 입력 처리를 Pointer Events 중심으로 정리해 마우스와 터치를 함께 다룸
  • 예전의 mousedown/mouseup/mousemove 방식은 포인터가 객체 밖으로 벗어나거나 밖에서 버튼을 놓을 때 드래그 상태가 꼬일 수 있어, 전역 핸들러 같은 보완이 필요했음
  • 터치 입력은 이벤트 전달 방식이 마우스와 다르며, 한 손가락 드래그가 페이지 스크롤까지 유발하므로 touchstart에서 preventDefault() 를 호출해 충돌을 줄임
  • 최종 레시피는 pointerdown, pointermove, pointerup, pointercancelsetPointerCapture() 를 조합하고, 왼쪽 버튼과 시작 오프셋을 확인해 객체가 갑자기 튀지 않게 함
  • 텍스트·이미지 포함 객체, 다중 터치, 중첩 드래그, <canvas>, hover와 drag 조합, lostpointercapture 같은 플랫폼별 예외는 실제 UI에서 별도 처리가 필요함

드래그 입력 핸들러의 목표

  • 많은 인터랙티브 페이지에는 독자가 직접 움직이는 드래그 가능한 객체가 있고, 다이어그램은 객체 위치 변화에 반응해야 함
  • 목표는 마우스와 터치 입력 모두에서 동작하는 입력 이벤트 핸들러를 만드는 것임
  • 기본 흐름은 pointerdown에서 드래그를 시작하고, pointermove에서 위치를 갱신하며, pointerup 또는 취소 이벤트에서 드래그를 끝내는 구조임
  • 실제 구현에서는 마우스 버튼, 여러 손가락 터치, 이벤트 전달 대상, 우클릭 컨텍스트 메뉴 때문에 단순 모델보다 복잡해짐
  • 기본 레시피는 다음 요소를 조합함
    • pointerdown에서 event.button !== 0이면 반환해 기본 버튼만 처리
    • 이벤트 좌표를 다이어그램 좌표로 변환
    • 시작 시 객체 위치와 포인터 위치의 차이인 dx, dy 저장
    • el.setPointerCapture(event.pointerId)로 포인터 캡처
    • pointermove에서 저장된 오프셋을 더해 state.pos 갱신
    • pointerup, pointercancel에서 state.dragging = null
    • touchstart에서 preventDefault() 호출
  • 이 레시피는 입력 이벤트 핸들러이며, 실제 출력 처리는 별도의 상태 핸들러와 연결해야 함
  • 완전한 코드는 examples page에 있으며, 객체 이동이 아닌 숫자 좌우 드래그, 캔버스 페인팅, 제한된 드래그 예도 포함됨
  • 테스트 범위는 Gecko/Firefox, Blink/Chrome, WebKit/Safari의 Mac, Windows, Linux, Android, iPhone, iPad임
  • hover 가능한 stylus, 하이브리드 터치+마우스 장치, 음성 입력은 테스트하지 않음
  • 이 방식은 요소를 다른 요소 위에 놓는 HTML Drag and Drop API가 아니며, 다이어그램에서는 마우스·터치 이벤트를 직접 읽어야 함

마우스 이벤트만 사용할 때의 한계

  • 초기 구현은 mousedown, mouseup, mousemove를 드래그 요소에 직접 붙이는 방식이었음
  • 겉보기에는 동작하지만 두 가지 문제가 생김
    • 포인터를 빠르게 움직이면 더 이상 원 위에 있지 않아 이벤트를 받지 못함
    • 원 밖에서 버튼을 놓으면 dragging 상태에 갇힐 수 있음
  • 개선책은 mousedown은 원에서 받고, 드래그 중 mousemovemouseupdocument 에 임시로 붙이는 방식임
  • mouseup에서 document에 붙인 mousemove, mouseup 핸들러를 제거하면 마우스 드래그가 더 안정적으로 동작함
  • 이 방식은 마우스만 처리하며 터치 이벤트는 다루지 않음

터치 이벤트의 차이

  • 터치 입력은 mousedown, mouseup, mousemove 대신 touchstart, touchend, touchmove를 사용함
  • 터치 이벤트는 touchstart 이후 자동으로 캡처되어 모든 touchmove가 원래 요소로 전달됨
  • 이 특성 때문에 마우스 이벤트처럼 document에 전역 이벤트 핸들러를 임시로 붙일 필요가 없음
  • 브라우저가 터치 시퀀스를 취소해야 할 때는 touchcancel을 보냄
  • 터치 드래그 구현은 touchstart에서 preventDefault()를 호출하고, touchmove에서 event.changedTouches[0] 좌표로 위치를 갱신함
  • 이 방식은 터치만 처리하며 마우스 이벤트는 처리하지 않음

Pointer Events로 마우스와 터치 통합

  • 마우스와 터치를 모두 처리하려면 많은 이벤트 핸들러가 필요했고, 2021년 전에는 그런 방식을 사용했음
  • 2011~2014년에는 d3 프로젝트에서 d3-drag를 사용했고, d3를 쓰지 않는 프로젝트에서는 별도 마우스+터치 코드를 만들었음
  • Pointer Events는 마우스와 터치 처리를 통합하고 단순화함
    • MS IE는 2012년에 지원을 추가
    • Chrome은 2017년, Firefox는 2018년, Safari는 2020년에 지원을 추가
  • 브라우저 동작은 시간이 지나며 바뀌었고, 2017년 Chrome은 일부 이벤트를 기본 passive mode로 바꿔 객체를 드래그하려 할 때 페이지가 스크롤되는 문제가 생김
  • Safari와 Firefox도 2018년에 같은 변경을 적용함
  • pointer capture를 쓰면 마우스 이벤트에서 필요했던 document 전역 핸들러 추가·제거 없이 단순한 로직으로 처리할 수 있음

Pointer Capture로 포인터 추적 유지

  • Pointer capture는 포인터가 원, 다이어그램, 브라우저 창 밖으로 벗어나도 추적을 계속할 수 있게 함
  • 마우스 이벤트에서는 같은 효과를 위해 document에 이벤트 핸들러를 붙여야 했지만, Pointer Events에서는 setPointerCapture(event.pointerId)가 더 단순함
  • 포인터 캡처가 없으면 빠르게 드래그하거나 다이어그램 밖으로 나갔다가 돌아올 때 드래그가 멈출 수 있음
  • 다이어그램 밖에서 버튼을 놓거나 Alt+Tab으로 다른 창으로 이동하는 경우도 캡처 유무에 따라 드래그 종료 처리가 달라짐
  • 터치 장치에서는 포인터 캡처가 기본 동작이지만, 마우스에서는 명시적으로 설정해야 함

터치 스크롤과 preventDefault()

  • 터치 장치에서 한 손가락 드래그는 기본적으로 페이지 스크롤을 일으킴
  • 같은 한 손가락 드래그가 원도 움직이면 스크롤과 객체 드래그가 동시에 발생함
  • 가장 단순한 해결책은 다이어그램에 CSS touch-action: none을 적용하는 것이지만, 이 경우 다이어그램 어느 곳에서도 스크롤할 수 없음
  • 원하는 동작은 원을 드래그할 때만 스크롤을 막고, 다이어그램 자체를 드래그할 때는 스크롤을 허용하는 것임
  • 비교 결과는 다음과 같음
    • 다이어그램 전체에 touch-action: none: 원 드래그와 다이어그램 드래그 모두 스크롤 중단
    • 기본값: 둘 다 스크롤을 막지 못함
    • 원에만 touch-action: none: 기대한 대로 동작하지 않음
    • touchstart에서 preventDefault(): 원 드래그 중 스크롤만 막고 다이어그램 스크롤은 허용
  • preventDefault()pointerstart가 아니라 touchstart에 붙임
  • 이 방식은 대부분 상황에서 동작하지만, desktop Firefox의 “mobile emulation mode”에서는 동작하지 않았고 Android의 mobile Firefox에서는 동작함

드래그 시작 오프셋 저장

  • 객체의 가장자리를 잡아 드래그할 때 객체 중심이 포인터 위치로 순간 이동하면 사용감이 나빠짐
  • 해결책은 드래그 시작 시 객체 중심과 포인터 위치의 상대 위치를 저장하는 것임
  • 이후 이동할 때 현재 포인터 좌표에 저장한 dx, dy를 더해 객체 위치를 계산함
  • 이 방식은 state.dragging을 단순 true/false가 아니라 {dx, dy} 같은 상대 위치 정보로 바꿈
  • 마우스에서는 가장자리에서 잡을 때 점프 차이가 잘 보이고, 터치에서는 손가락이 가려서 덜 보일 수 있음
  • 관련된 작은 디테일은 little details 페이지에도 정리되어 있음

컨텍스트 메뉴와 마우스 버튼

  • 컨텍스트 메뉴 활성화 방식은 플랫폼마다 다름
    • Windows: 우클릭 down+up, Shift+F10
    • Linux: 오른쪽 버튼 down, Shift+F10
    • Mac: 오른쪽 버튼 down, Ctrl+왼쪽 클릭
    • iOS: 텍스트에서만 long press
    • Android: 어디서든 long press
  • 문제는 pointerdown은 보이지만 pointerup은 항상 보이지 않는 경우가 있다는 점임
  • 버튼이 실제로는 올라갔는데 코드에서는 아직 눌린 상태로 판단할 수 있음
  • 대응 방식은 오른쪽 버튼을 무시하고 왼쪽 마우스 버튼에서만 dragging 상태를 설정하는 것임
  • Pointer Events 명세의 button 속성에서 button = 0은 primary button을 뜻하며, 이를 사용해 가운데·오른쪽 버튼을 제외함
  • Mac의 Chrome과 Safari에서는 Ctrl+클릭이 왼쪽 버튼 pointerdown처럼 들어올 수 있어 event.ctrlKey도 확인해야 함
  • 선택지는 다음과 같음
    • contextmenu에서 preventDefault()로 메뉴를 막을 수 있지만, 기본 중간 클릭이나 드래그 동작과 충돌할 수 있음
    • 왼쪽 버튼만 드래그하면 대부분의 우클릭 차이를 피할 수 있음
    • Mac Ctrl+클릭까지 피하려면 ctrlKey도 제외해야 함
  • 버튼 변화가 pointerdown이나 pointerup으로 전달되지 않아도 pointermove의 버튼 상태로 일부 확인할 수 있음

텍스트와 이미지가 포함된 객체

  • 드래그 가능한 요소 안에 텍스트나 이미지가 있으면 추가 처리가 필요함
  • 원을 드래그하는 동안 내부 텍스트가 선택될 수 있음
  • 해결책은 CSS user-select: none을 적용하는 것임
    • 항상 적용하면 내부 텍스트가 언제나 선택되지 않음
    • 드래그 중에만 적용하면 평소에는 텍스트 선택이 가능함
  • 2023년 초 기준 Safari는 unprefixed user-select를 아직 지원하지 않아 webkitUserSelect도 함께 설정해야 함
  • Windows, Linux, Mac은 텍스트와 이미지의 애플리케이션 간 drag and drop을 지원하며, 이것이 객체 드래그와 충돌함
  • dragstart에서 preventDefault()를 호출하면 선택된 텍스트나 이미지 드래그보다 객체 드래그를 우선할 수 있음
  • 브라우저와 운영체제에 따라 동작은 조금씩 다름

여러 포인터와 동시 드래그

  • 여러 손가락이나 여러 마우스로 여러 객체를 동시에 드래그할 수 있는지는 엣지 케이스임
  • 터치에서는 제시된 코드가 여러 객체를 동시에 드래그하는 데 이미 동작함
  • 두 손가락으로 같은 객체를 드래그하는 경우는 별도 처리가 필요함
  • 해결책은 pointerdown에서 event.pointerIdstate.dragging 또는 상태에 저장하고, pointermove에서 같은 pointerId가 아니면 무시하는 것임
  • 이 구현 예시는 본문에 완전히 적용되어 있지 않고, canvas dragging test에서 확인할 수 있음
  • 마우스의 경우 Pointer Events 명세는 일반 운영체제와 사용자 에이전트가 여러 마우스 입력 개념을 보통 갖지 않는다고 밝힘
  • 트랙패드와 외장 마우스가 함께 있어도 일반적으로 하나의 마우스 포인터로 취급되므로, 서로 다른 마우스로 다른 객체를 드래그할 방법은 없다고 판단함

여러 마우스 버튼을 함께 누를 때

  • 여러 마우스 버튼을 동시에 누르면 Pointer Events와 Mouse Events의 동작이 달라짐
  • Mouse Events는 각 버튼 누름마다 mousedown, 각 버튼 해제마다 mouseup을 보냄
  • Pointer Events 명세에 따르면 처음 눌린 버튼에서 pointerdown, 마지막으로 해제된 버튼에서 pointerup이 발생함
  • 이 때문에 pointerdownpointerup이 서로 다른 버튼 상태에서 발생할 수 있음
  • 예를 들어 왼쪽 버튼을 누르고, 오른쪽 버튼을 누른 뒤, 왼쪽 버튼을 놓으면 객체가 계속 드래그 상태로 남을 수 있음
  • 해결책은 pointermove에서 event.buttons & 1로 왼쪽 버튼 상태를 확인하고, 더 이상 눌려 있지 않으면 end(event)를 호출하는 것임
  • 포인터 캡처는 모든 버튼을 놓을 때까지 계속될 수 있으며, 명시적으로 캡처를 해제하지 않는 한 이어질 수 있음
  • 이 구현은 이 문제와 여러 엣지 케이스를 모두 처리하지는 않음

중첩 드래그 요소

  • 드래그 가능한 요소 안에 또 다른 드래그 가능한 요소가 있으면 안쪽과 바깥쪽 요소가 모두 드래그를 처리함
  • 해결책은 안쪽 드래그 요소에서 event.stopPropagation()을 호출해 이벤트가 바깥 드래그 요소로 올라가지 않게 하는 것임
  • pointerdownpointermove에서 모두 전파를 중단하는 방식이 쓰임
  • 데모는 본문에는 없지만, Vue playground 예시에서 빨간 드래그 요소가 노란 드래그 요소의 자식인 사례를 확인할 수 있음

Canvas에서의 드래그

  • 보통 SVG를 사용하지만, <canvas>에서는 드래그 가능한 요소 자체에 이벤트 핸들러나 마우스 포인터 모양을 설정할 수 없음
  • 2D Canvas나 WebGL에서는 이벤트 핸들러를 <canvas>에 붙이고, 좌표가 드래그 가능한 객체 위인지 직접 판정해야 함
  • 처리 방식은 다음과 같음
    • pointerdown, touchstart, dragstart: 드래그 가능한 객체 위가 아니면 즉시 반환
    • pointermove: 드래그 가능한 객체 위인지에 따라 커서 설정
  • 데모는 examples page에 있음

마우스 Hover와 터치 Drag 조합

  • 어떤 다이어그램은 마우스에서는 버튼을 누르지 않은 hover로 반응하고, 터치 장치에서는 hover가 없으므로 drag로 반응해야 함
  • 기본 레시피는 마우스와 터치 모두 drag를 기대함
  • 마우스 hover와 터치 drag를 함께 쓰려면 pointermove에서 if (!state.dragging) 조건을 제거함
  • Responsive Design page에서는 마우스로 행 위에 hover하면 레이아웃이 바뀌고, 터치에서는 drag로 레이아웃이 바뀜
  • Hexagons Guide에서도 많은 다이어그램이 마우스 hover와 터치 drag를 함께 사용함
  • 터치에서 항목 A에서 B로 드래그할 때 캡처가 유지되면 move 이벤트가 A로 전달되므로, B를 하이라이트하려면 캡처 해제가 필요할 수 있음

Toggle Paint와 드래그 상태 확장

  • Rounded Cell Painter에서는 pointerdown이 초기 페인트 색상을 캡처함
  • 이후 pointerup까지 모든 pointermove는 같은 페인트 색상을 사용함
  • 이를 위해 state.dragging은 초기 x, y뿐 아니라 초기 페인트 색상도 포함해야 함
  • 데모는 examples page에 있음

lostpointercapture 엣지 케이스

  • lostpointercapture는 포인터 캡처를 잃었는지 감지하는 데 사용할 수 있음
  • 어떤 상황에서 발생하고 무엇을 해야 하는지는 아직 완전히 정리되지 않음
  • 댓글에서 다룬 시나리오는 다음과 같음
    • 왼쪽 버튼으로 원 드래그 시작
    • 왼쪽 버튼을 누른 채 드래그 영역 밖으로 이동
    • 오른쪽 버튼을 누른 뒤 왼쪽 버튼을 놓음
    • 드래그 영역 밖 아무 곳이나 클릭
  • standard는 포인터 캡처가 해제된 뒤 사용자 에이전트가 lostpointercapture 이벤트를 반드시 발생시켜야 한다고 함
  • 실제 브라우저 동작은 크게 다르며, 일부 시스템에서는 lostpointercapture가 발생하지만 일부는 다른 곳을 클릭할 때까지 발생하지 않음

댓글과 토론

Hacker News 의견들
  • 드래그를 다룬 글인데, Amit이 말한 함정들을 거의 다 겪었고 같은 해법에 도달했음. 훌륭한 글임
    사용자 입장에서는 드래그로 순서 바꾸기가 너무 자연스러워 보이지만, 크기가 제각각인 항목, 항목 사이 자리표시자 생성, 가장자리 감지, 박스 맞춤 알고리즘의 토끼굴까지 들어가면 직접 구현하기 꽤 어려운 축에 듦

    • 이럴 때 좋아하는 꽤 일반적인 요령이 있음: 사용자가 드래그를 시작하면 가능한 모든 드롭 대상, 또는 현재 보이는 대상의 레이아웃을 계산하고, 각 레이아웃에서 드래그 중인 객체가 끝나는 위치를 기록함
      마우스가 움직일 때마다 그 위치들 중 현재 드래그 객체 위치에 가장 가까운 것을 고르고, 선택된 레이아웃을 렌더링하면 됨. 복잡한 레이아웃이나 리플로우에서도 느낌이 좋게 작동함
    • 내 인덱스 카드 앱 Card Buddy에서 이 문제를 풀어야 했음. [1] 재미있는 도전이었고, 나중에는 더 나은 방법도 찾음
      최종적으로는 카드를 집어 들면 그 카드가 보드에서 삭제된 것처럼 레이아웃을 계산했고, 그러면 쉬워짐. 마우스를 올린 위치에 있던 것들을 밀어내기만 하면 됨
      그래도 예외 처리는 엄청 많았고, 특히 아직 데이터 모델에 “커밋”되지 않은 새 카드를 편집하기 시작할 때 까다로웠음. 기존 카드들을 밀어내서 유령 카드 자리를 만들 수 있는 옵션도 추가해야 했음
      수동으로 처리해야 할 예외가 많다는 걸 인정하는 게 도움 됨. 더 일반적이고 균질한 해법이 있을 것처럼 접근하면 설계에서 빙빙 돌게 됨. 언젠가 다양한 예외 사례를 블로그 글로 정리해야 할 듯함
      더 나은 레이아웃 방식도 찾았는데, 불필요한 계산을 줄이고 레이아웃 엔진을 더 유연하고 사용자 친화적으로 만들어 줌. 데이터 모델 표현을 어떻게 고르느냐가 해법에 엄청난 차이를 만든다는 게 놀라움
      [1] https://www.ussherpress.com/cardbuddy/
    • 여기에 중첩까지 지원하는 드래그 순서 변경이면 더 힘들어짐
    • 같은 생각임. 아이디어는 단순한데 구현은 잔인할 수 있음. 드래그 선택도 몇 년 동안 너무 당연하게 여겼음
  • 글에서 취소 처리는 다루지 않는 것 같음. 예를 들어 Windows에서는 Escape를 누르면 드래그가 취소되는 관례가 있음
    때로는 정의된 영역 바깥에서 마우스를 놓으면 드래그를 취소하고 싶기도 함. 취소는 사용자에게 더 빠른 실행 취소, 또는 “아, 사실 드래그하려던 게 아니었네” 같은 탈출구가 됨
    그래서 별도의 실행 취소 기능이 없더라도, 드래그 시작 시점의 원래 상태를 저장해 두었다가 취소되면 복원해야 함
    영역 밖 드래그 취소의 경우에는 취소 해제도 있음. 포인터가 영역 밖에 있을 때는 원래 상태로 되돌아간 것처럼 시각적으로 보여 주다가, 포인터가 다시 영역 안으로 들어오면 드래그를 재개하는 방식임. 실제 취소는 마우스를 놓을 때 일어나지만, 드래그 중에도 취소될 것임을 미리 시각적으로 알려 주는 셈임

    • Windows에서 어떤 것을 너무 멀리 드래그하면 드래그 중인 객체와 포인터가 갑자기 사라지고, 경고 없이 원래 위치로 튕겨 돌아가는 동작이 있음
      Microsoft가 아직도 이걸 좋은 UI 설계라고 믿는다는 게 믿기지 않음. 사용자를 완전히 혼란스럽게 만들고, 무슨 일이 일어나는지 몰라 왼쪽 마우스 버튼을 놓기를 겁내거나 요요처럼 반복하는 상황도 볼 수 있음
      취소는 자주 간과되는 큰 문제이며, 단순하지 않다는 데 동의함. 일반 사용자는 ESC가 도움 된다는 것도 모름
    • 다른 플랫폼에서 ESC 말고 처리해야 할 취소 키가 따로 있는지 궁금함
  • Ardour [0]의 GUI에서 온갖 객체를 드래그하는 일을 20년 해오며 배운 추가 세부사항이 있음: 모든 버튼 누름과 뗌 이벤트를 움직임이 없는 드래그 이벤트로 처리해야 함
    즉, 누름은 항상 드래그를 시작하고 뗌으로 끝나며, 뗌을 처리하는 코드가 움직임 없음 조건을 특수 처리하는 방식임
    [0] https://ardour.org/

    • 그 방식도 작동하겠지만, 나는 그렇게 하지 않았고 기본만 제대로 잡은 뒤에는 문제가 없었음
      일반적인 mousedownmouseup 처리기를 두고, 클릭인지 드래그 시작인지 또는 다른 동작인지 판단하려고 약 150ms의 설정 가능한 타임아웃을 사용함. mouseup이 발생하거나 마우스가 화면을 벗어났고 진행 중인 드래그가 있으면 stopdrag함. 내게 드래그는 여러 특수 사례 중 하나일 뿐임
    • 그렇게 하는 이유가 궁금함
  • 아주 오래전 Minneapolis에 NSF가 지원한 geometry center가 있었음. 계산 군론 학회가 열렸고, 주변 분야에서 마스코트 같은 사람 몇 명을 초대했음
    나는 대수기하학 시스템을 작성한 덕분에 초대를 받았고, -20°F 추위에 아주 일찍 일어나 그날 쓸 Silicon Graphics 워크스테이션을 확보한 뒤 군 생성자와 관계를 더 잘 이해하기 위한 게임을 코딩했음
    그 게임에는 드래그가 들어갔음. 그 결과 새벽 2시에 이어진 대화들이 좋았음. 내 생각은 드래그가 현실 세계의 물리 법칙을 따를 필요는 없다는 것이었고, 드래그는 아주 강한 LSD 탭 같은 느낌이어야 한다고 봤음. 모두가 여기에 빠져들었고, 각자 아이디어를 냈음

  • 글은 웹 브라우저에서 생기는 여러 예외를 제쳐 두면 비교적 기본적인 드래그를 구현함. 가이드라인에 스냅하기, 경계나 다른 객체와의 충돌 막기, 드래그 작업에 반응해 크기나 위치가 바뀌는 애니메이션 드롭 대상 같은 제약이 있는 드래그 자료가 있는지 궁금함
    예전에 원형 시계를 서로 다른 길이의 쐐기 조각으로 나눠 집중/휴식 구간을 정의하는 맞춤형 Pomodoro 타이머 UI를 만들고 싶었음. 그 쐐기 조각들의 드래그 순서 변경 구현은 별로 진척되지 않았음

    • 이 코드 방식에서는 드래그 움직임이 어떤 상태를 갱신함. 상태를 설정할 때 제약 조건을 적용할 수 있고, 그 상태가 다시 표시를 구동함
      제약 시스템을 이벤트 처리 시스템과 분리하고 싶었음. jquery-ui 같은 라이브러리는 둘을 묶어 버려서 이벤트 처리 시스템이 가능한 모든 제약을 알아야 함. jquery-ui는 경계 상자, 축, 정사각 격자, DOM 요소로의 스냅을 지원함. 그런데 육각 격자, 로그 스케일 격자, 원형 경계로 스냅하고 싶으면 지원되지 않음
      코드에서 state.pos = …를 볼 수 있는데, 거기가 상태를 설정하는 곳임. 제약에는 pos를 setter 뒤에 숨김. 그러면 드래그 이벤트 처리 코드가 어떤 제약이 필요한지 몰라도 setter가 제약을 적용할 수 있음
      페이지를 업데이트해서 제약 예시를 보여 줘야겠음. 코드 레시피에서 이 측면을 언급하는 걸 완전히 잊었음
      오래된 제약 예시는 https://www.redblobgames.com/articles/curved-paths/making-of...에 있고, 충돌 방지 예시는 https://redblobgames.github.io/circular-obstacle-pathfinding...에 있음. 다만 드래그 순서 변경이나 애니메이션 드롭 대상은 아직 시도해 보지 않았음
  • 이 사이트에는 나도 고마운 게 있음. Uber에서 지리 분석용 육각형 시스템을 설계할 때 https://www.redblobgames.com/grids/hexagons/를 많이 참고했음

  • 제대로 된 상호작용을 다룰 때 나오는 함정과 주의점을 잘 정리한 글임
    더 “바로 쓸 수 있는” 쪽으로는 여러 프로젝트에서 interactjs를 꽤 오래 사용해 왔음

  • 모두에게 감사함. 오늘 HN에 올라와서 놀랐고, 질문에 답할 수 있음

    • HTML 내장 draggable="true" 속성과 드래그 이벤트를 살펴봤는지 궁금함
      내장 기능이 이 코드와 비교해 부족한 점이 있는지도 궁금함
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement...
      https://developer.mozilla.org/en-US/docs/Web/HTML/Global_att...
    • JavaScript 드래그에 관한 훌륭한 논의였고 정보가 정말 유용했음
      빠른 질문이 있음. dragstart 같은 내장 DOM 이벤트로 드래그 움직임을 한 축으로 제한하려면 어떻게 해야 하는지 궁금함. dragstart/dragenter/dragover/drop 이벤트로 드래그 앤 드롭 기능을 구현했지만, 움직임을 x축으로 제한하는 빠른 방법을 찾지 못했음. JQuery의 드래그 앤 드롭 API는 이를 지원했던 것으로 기억함. 네이티브 DOM 이벤트/API만 쓰려 하고 있어서 관련 정보나 포인터가 있으면 좋겠음
    • 예전에는 브라우저 UI 코드를 항상 작성했지만, 몇 년 쉬었음. 누군가 포인터 이벤트를 쓰는 건 처음 들어봄
      Safari가 2020년에 지원을 추가했다는 점을 고려하면, 브라우저가 이를 지원하지 않을까 걱정하지 않는지 궁금함. macOS Mojave 사용자도 Safari 13이나 14를 쓰고 있을 테니 Safari 12는 실제로 더는 쓰이지 않기를 바라지만, 일반 웹 사용자를 대상으로 하는 프로덕션 앱에서 드래그처럼 단순한 기능이 안 되면 꽤 나쁠 것임
      드래그 중 document에 이벤트 처리기를 추가하는 건 오래된 관행이고, 브라우저는 어떤 사용 사례를 단순화하려는 새 기능을 계속 추가하지만 각자 예외와 함정이 있음. 글에서도 아직 완전히 다루지 않았다고 말함. 최종 결과에도 포인터 이벤트와 터치 이벤트 조합이 남아 있음
      코드가 줄어든다는 의미의 “단순함”이 추가 예외, 낮은 브라우저 지원, 개발자가 마우스 이벤트보다 덜 이해되고 문서화됐을 법한 포인터 이벤트의 세부사항과 브라우저 차이를 알아야 하는 비용을 감수할 만큼 가치가 있는지 궁금함
  • 빠진 게 하나 있음: 접근성. 키보드 조작으로 드래그를 어떻게 가능하게 해야 할까?

    • 여기서는 조금 빗나간 질문 같음. 그림 프로그램에서 키보드 사용자가 캔버스를 패닝하려고 클릭한 채 유지하는 동작을 어떻게 하느냐고 묻는 것과 비슷함. 그렇게 하지 않음. 마우스로 패닝하거나 객체를 드래그하는 건 하나의 인터페이스일 뿐이고, 과제는 캔버스 패닝이며 이를 달성하는 방법은 다양함
      내 예에서 접근성의 답은 해당 작업을 완료할 키보드 조작을 추가로 제공하는 것임. 가능하면 키를 계속 누르고 있지 않아도 되는 방식이 좋음. 예를 들어 단축키로 패닝 모드에 들어가고, 화살표 키로 뷰포트를 캔버스 주변으로 이동시키면 해결됨
      드래그 가능한 객체의 경우 작업은 순서 변경임. 이를 접근 가능하게 하려면 순서를 바꾸는 대체 수단을 제공하면 됨. 예를 들어 TAB으로 객체들 사이의 포커스를 순환하고, 키로 포커스된 객체를 선택한 뒤, 화살표 키로 드래그 미리보기를 해당 방향의 가장 가까운 유효 위치로 옮기는 식임
    • 대체로 답은 “그렇게 하지 않는다”에 가까움. UI는 포인터나 터치 장치를 쓸 수 없는 사람도 페이지와 상호작용할 수 있게 드래그의 대안을 제공해야 함
      즉, 드래그 구현 안에 키보드 지원을 억지로 끼워 넣지 말고, 드래그 기능과 별도로 의미 있는 키보드 기능을 추가해야 함
    • 그건 이동 가능한 객체가 될 것이고, 이 구현을 기반으로 쉽게 만들 수 있을 것 같음
    • TAB / Space / Enter로 항목을 선택하고, Ctrl + 화살표 키로 “드래그 중인” 요소를 움직이면 됨. 당연히 키를 한 번 누를 때 한 블록씩만 이동 가능함
  • 좋음. 이번 주 초 Vue 앱에서 SVG 요소 드래그를 구현하고 있었는데, 글쓴이가 설명한 거의 모든 것을 같은 순서로 직접 발견해야 했고, 최종적으로도 거의 같은 컴포넌트가 됐음
    다만 나는 함수형 컴포넌트 대신 useDragging이라는 composable 유틸리티로 작성했음

    • 멋짐. directive와 슬롯이 있는 컴포넌트는 시도해 봤지만, composable은 아직 안 해 봤음. 그 방식으로 이벤트 처리기를 어떻게 설정해야 할지 확신이 없었기 때문임. composable 경험이 많지는 않음