javascript drag and drop 구현 시도와 시행착오

Tony·2021년 9월 11일
4

javascript

목록 보기
30/61
post-custom-banner

서론

이 간단해 보이는 drag and drop animation을 만들어 보려했지만 생각보다 쉽지 않았다.
개인적인 생각으론 인프런에서도 sortable js 라이브러리를 사용한 것으로 생각된다.
왜냐하면 선택 후 드래그로 움직임을 바꿀 때 현재 선택된 element(브라우저에서 $0)에 해당하는 객체가 선택된 다음 객체로 이동되는 것이 같았고
sortable-ghost라는 클래스가 붙는 것도 동일했다.
그 외 draggable이 마우스를 클릭할 때 false에서 true로 바뀌는 점,
애니메이션도 같은 것을 확인할 수 있었다.

transition: transform 100ms ease 0s;
transform: translate3d(0px, 0px, 0px);

본론

1. drag and drop에 대한 학습

이전에 drag and drop api를 사용해본 적이 없었다.
그래서 먼저 drag and drop이 어떤식으로 동작하는지 알아야 했다.
구글에서 간단한 예제 부터 따라해보았다.
간단한 드래그앤 드랍 예제1
애니메이션 없는 드래그앤 드랍 리스트

1-1. drag and drop event sequence

drag 에서 drop까지 발생되는 이벤트의 순서는 다음과 같다.

  1. dragstart
  • drag를 시작할 때 최초 1회만 실행 됨
  1. dragover
function onDragOver(event) {
  event.preventDefault(); // drop이 가능하다는 표시를 해줌
}
  • drag 중 계속 실행됨
  • event.preventDefault();를 해줘야 drop이 가능하다는 작은 네모표시가 생긴다. 그렇지 않은 경우엔 🚫표시가 생긴다.
  • event.preventDefault();만 하고 계속실행되어야 될 함수는 없다고 생각되어서 이 이벤트 콜백함수엔 이것 외에 작성한 코드는 없었다.
  1. dragenter
  • dragstart이후 element를 만날 때 마다 실행됨
    • 자기 자신도 포함
      • dragenter의 콜백함수에서 자기 자신일 경우 early return시켜서 다른 리스트를 만날 경우에 이동할 때 사용을 했다.
  • 자신의 childNode 또는 다른 element의 childNode에 drag중인 상태로 마우스가 들어가도 실행이 된다.
  1. drop
  • drag를 끝내려고 마우스를 떼는 순간(mouseup) 발생 된다.

1-2. 위치 바꾸기

dragstart의 event.target와 // selectedNode
dragenter에서 selectedNode가 아닌 event.target의 위치를 바꾸면 위치를 바꿀 수 있었다.

1-2-1. 위치를 바꾸는 방법 1.

parentNode.insertBefore(newNode, referenceNode);

1-2-2. 위치를 바꾸는 방법 2.

referenceNode.before(newNode) 또는
referenceNode.after(newNode)
element.before()
element.after()

  • 이 방법이 insertBefore보다 가독성이 좋아서 이 방법을 사용했다.
1-2-3. 위치를 바꾸는 방법 3.

let childrenArray = Array.prototype.slice.call(parentNode.children);
parentNode의 children을 배열에 담아서 selectedNode index와 자리를 바꾸려는 Node index를 찾아서 배열 내 에서 순서를 바꾼 뒤 parentNode에 다시 append하면 순서를 바꿀 수 있었다.

// HTML
<ul id="container" class="wrapper" ondragover="onDragOver(event)">
      <li data-id="1" class="item" draggable="true">
        <span class="text">Draggable Element One</span>
        <i class="fas fa-bars"></i>
      </li>
      <li data-id="2" class="item" draggable="true">
        <span class="text">Draggable Element Two</span>
        <i class="fas fa-bars"></i>
      </li>
      <li data-id="3" class="item" draggable="true">
        <span class="text">Draggable Element Three</span>
        <i class="fas fa-bars"></i>
      </li>
    </ul>

// javascript
const liList = container.children;
let liArray = Array.prototype.slice.call(liList);
// 배열의 순서 변경 코드는 생략 함
liArray.forEach(item => {
  container.append(item);
});

이 방법은 노아님이 추천해준 영상의 유튜버가 사용한 방법이었다.

  • 처음에 이 방법을 보고 애니메이션이 끝난 후 바로 위치를 바꾸게 했으나 애니메이션이 연속으로 발생되어야 하는 상황(두개 이상을 빠르게 통과할 때)는 잘 동작하지 않아서 바꾸었는데 문제는 이게 아니었다.

그렇지만 영상처럼 모든 자리이동 애니메이션이 끝난 후 mouseup 이벤트에서 DOM의 순서만 바꿔줄 때 사용한다면 좋은 방법이다.
그렇지만 조건으로 모든 chilren이 absolute 상태에서 top을 직접 부여하는 방식으로 위치를 조정해야 한다.
translateY이 아닌 top을 사용한다면 이 방법으로 다시 시도해볼만 할 것 같다.

1-3. drag and drop animation

그러나 처음 따라한 예제들은 드래그앤 드랍의 이벤트 흐름에 대한 이해는 도왔지만 애니메이션을 어떻게 줄지에 대한 도움은 없었다.

그래서 애니메이션은 transition과 transform의 translateY를 사용하면 쉽게 될 것만 같았지만 그것은 나의 착각이었다.

첫 번째 문제 drag and drop으로 Node순서를 바꾸면
애니메이션이 없는 경우엔 상관이 없지만
애니메이션을 주려고 하는 경우, 바뀐 위치에서 엉뚱한 방향으로 애니메이션이 실행되었다.

이를 해결 하기 위해 translateY(distancepx)를 거꾸로 기존 포지션에 있는 곳으로 위치 시킨다음 애니메이션과 함께 translateY(0px)을 주려고 했으나
하나의 함수가 끝나기 전에 DOM에 바로 코드가 적용되는 것이 아닌 그 함수와 관련된 모든함수들이 끝난 다음 DOM의 위치가 조정이 되었다.

function onDragEnter(event) {
  //...
  changePosition(li, theOtherElementTop); // 서로 위치를 바꿈
}

function changePosition(li, theOtherElementTop) {
	const distance = theOtherElementTop - selectedElementTop;
    if (distance > 0) {
    // 위에서 아래로 내려올 때
    selectedElement.before(li);
    selectedElement.style = `transform: translateY(${-distance}px)`;
    li.style = `transform: translateY(${distance}px)`;
  } else {
    // 아래에서 위로 올라갈 때
    selectedElement.after(li);
    selectedElement.style = `transform: translateY(${-distance}px)`;
    li.style = `transform: translateY(${distance}px)`;
  }
  addAnimation(li);
}

function addAnimation(li) {
  selectedElement.classList.add('animation'); // 얘는 css가 나중에 적용되었고
  li.classList.add('animation'); // 얘는 css가 먼저 적용 됨 - 애니메이션 적용 안됨
}

2. 애니메이션이 포함된 예제 발견

이후 노아님이 추천해준 유튜브 영상을 보았지만 그 방법으로 드래그 앤 드랍 애니메이션을 줄 수 있었지만 인프런과 동일한 방법은 아니었다.
사실 어떤 방법으로든 구현을 해도 사용자 경험은 비슷할 것이다.
그렇지만 개인적으로 인프런방식으로 구현을 해보고 싶은 오기가 생겨서 계속 스스로 답을 찾으려고 여러가지 시도를 했던 것 같다.

3. 구현을 위한 시도

마우스 클릭 이후 드래그를 시작하면 dragstart 이벤트가 실행된다.
dragstart는 drop전까지 한번만 발생되기 때문에 dragstart의 event.target은 내가 선택한 것이라는 것을 다른 곳에서 기억할 수 있게 변수에 저장을 해두고 drop이 되면 저장한 변수를 초기화하는 방법을 생각했다.

dragenter에서 비교 후 애니메이션을 주고 노드 위치를 애니메이션이 끝날 때 setTimeout을 사용해서 바꿔주려했으나 하나씩 할땐 문제가 없지만 두개 이상 연속으로 지나가면서 애니메이션이 발생되어야 할 때 두번째 부턴 애니메이션이 동작하지 않는 문제가 있었다.

DOM을 먼저 바꾼 뒤 바뀌기 전 위치로 translateY로 이동 시킨 후 다시 원점으로 이동하면서 애니메이션을 동작시키려 했으나 1-3. 의 문제가 있었다.

  • 먼저 이동 시킨 후(translateY(distance)) 나중에 애니메이션과 원점으로 이동하려 했으나(transition: transform transitionTimems;transform:translateY({transitionTime}ms; transform: translateY({0}px))
  • translateY(distance)이 먼저 적용되고 애니메이션이 나중에 적용되는 것이 아닌 두개가 한번에 적용되어 화면에 렌더링이 되어서 translateY(${0}px)만 적용되는 문제가 있었다.
  • HTML위치 변경 후 애니메이션을 주려고 했던 코드

결론

더 해야할까?

sortable js 라이브러리와 같이 하나씩 즉시 위치가 바뀌는 방식으로 구현할 방법을 잘 모르겠다.
애니메이션 중간에 위치 변경이 있다면 애니메이션이 실행되고 있는 함수를 취소하고 새로운 애니메이션이 동작하도록 하는 방법을 구현해야 될 것 같았는데 그것이 가능한지도 모르겠다.

  • DOM 좌표가 변경되면 애니메이션은 변경된 좌표를 기준으로 실행되기 때문

다른 방법으론, 노아님이 추천해준 영상과 같은 방법으로 구현한다면 가능할 것 같기도 하다.

  • DOM순서는 애니메이션이 끝난 후 mouseup 이벤트에서 한번에 변경하는 방식

어차피 유튜버 영상의 코드와 방식으로 구현한다면 라이브러리를 사용하는 것과 별 차이가 없을 것 같다는 생각도 든다.
오히려 라이브러리가 더 좋은 대안이 될 것이다.

얻은 것과 해야할 것

비록 완벽하게 구현하는 것엔 실패를 했지만 한칸씩 이동하는 것은 스스로 만들어봤고
drag api를 전혀 사용하지 않는 유튜버의 방식으로 구현할 수 있다는 것도 알게 되었다.
이번 기회로 브라우저 web api를 더 많이 알게 되었다.
그리고 이동하는 중간에 애니메이션이 없이 단순이 이동할 자리에 border-bottom으로 어디에 들어갈지 표시하는 정도의 드래그앤 드랍은 구현할 수 있게 되었다.

생각보다 되게 쉬워보였지만 sortable js에선 이것을 구현하기 위해 많은 노력과 고민이 있었다는 것을 깨달았다.
그래도 간단한 드래그 앤 드랍에 대한 이해와 고민을 충분히 해볼 수 있는 좋은 경험이었던 것 같다.
실제 프로젝트에선 라이브러리를 사용해서 구현을 하고 그것은 다음에 글로 정리해서 올릴 예정이다.

참고 문헌

profile
움직이는 만큼 행복해진다
post-custom-banner

0개의 댓글