(React) Drag & Drop 구현하기

호두파파·2022년 2월 27일
19

React

목록 보기
29/38


원티드 X 코드스테이츠 1주차 과정을 마쳤다. 이번 포스팅은 기업과제를 하면서 내가 수행한 파트에 대한 내용이다.

Drag & Drop 구현하기

기업과제로 주어진 내용은 Dual Selctor를 구현해서 좌우로 콘텐츠를 넘기는 내용이었다.
그중 나는 같은 엘리먼트에 위치한 콘텐츠들을 마우스 이벤트로 드래그 & 드랍하는 부분을 작성하기로 했다.

마우스 이벤트

mouseover

사용자가 마우스를 해당 element 바깥에서 안으로 옮겼을때 발생한다.

mouseout

사용자가 마우스를 해당 element 안에서 바깥으로 옮겼을 때 발생한다.

mouseEnter

사용자가 마우스를 해당 element 바깥에서 안으로 옮겼을때 발생한다. (버블링이 발생하지 않는다.)

mouseleave

사용자가 마우스를 element 안에서 바깥으로 옮겼을때 발생한다. (버블링이 발생하지 않는다.)

❗️ mouseOver, mouseOut와 mouseEnter, mouseLeave의 차이점

이벤트 버블링 효과가 나타나지 않는다는 점에서 차이가 있다.
자식요소에 mouseOver, mouseOut 이벤트가 등록되어 있지 않더라도 부모요소에 등록되어 있다면, 이벤트 버블링 효과 때문에 자식 요소에 이벤트가 등록된 것처럼 동작한다.


위 4개 이벤트 개념이 리스트 요소간 Drag & Drop을 구현하는데 사용되는 개념이다.

💻 코드작성하기

🖱 드래그와 관련된 이벤트 : onDragStart

const draggingItemIndex = useRef(null);
const draggingOverItemIndex = useRef(null);

const onDragStart = (e, index, id) => {
  draggingItemIndex.current = index;
  e.target.classList.add('grabbing');
};

useRef객체의 current 프로퍼티 값은 리랜더링되더라도, 초기화되지 않는다. 이 곳에 드래그될 엘리먼트의 인덱스(draggingItemIndex)와 그 아이템이 최종적으로 지나친 엘리먼트의 인덱스 값(draggingOverIndex)을 저장해준다.

🖱 드래그와 관련된 이벤트 : onDragEnter

const onAvailableItemDragEnter = (e, index) => {
  draggingOverItemIndex.current = index;
  const copyListItems = [...availableOptionsArr]; // 1
  const dragItemContent = copyListItems[draggingItemIndex.current]; //2
  copyListItems.splice(draggingItemIndex.current, 1); //3
  copyListItems.splice(draggingOverItemIndex.current, 0, dragItemContent); // 4
  draggingItemIndex.current = draggingOverItemIndex.current;
  draggingOverItemIndex.current = null; //5
  setAvailableOptionsArr(copyListItems);
}; //6

드래그 중일때 실행되는 이벤트이다. onDragStart 직후부터 onDragEnd 직전까지 계속 호출된다.

  1. 먼저 얄은 복사로 렌더링 시 참조하고 있는 데이터 배열을 복사해준다.
  2. 복사된 배열에 useRef객체에 저장된 인덱스값을 참조해 드래그 중인 아이템의 인덱스마다 값을 업데이트 시켜준다.
  3. 드래그 된 아이템은 이동 중이기 때문에, 랜더링에 참조할 복사배열에서 값을 제거해준다.
  4. 드래그된 아이템을 드래그 오버된 아이템 다음으로 위치할 수 있도록 splice() 메소드를 사용한다.
  5. 드래그 오버된 아이템의 인덱스를 참조하기 위해 만들어준 useRef객체의 current값을 초기화해준다.
  6. 리스트를 새롭게 렌더링할수 있도록 상태를 업데이트해준다.

🖱 드랍과 관련된 이벤트 : onDragEnd

const onDragEnd = (e) => {
  e.target.classList.remove('grabbing');
};

마우스를 올려놓았을때 드래그가 끝나면서 실행되는 이벤트이다.

🖱 드래그와 관련된 이벤트 : onDragover

const onDragOver = (e) => {
  e.preventDefault();
};

이벤트 전파를 방지할 수 있도록 e.preventDefualt()를 작성해준다. e.preventDefualt()을 사용하면 브라우저의 기본 동작을 막을 수 있다.

e.stopPropagation을 이용해서 이벤트 버블링을 막을 경우, click이베느를 감지할 수 없는 deadZone이 발생한다. 자세히 살펴보기


구현 모습

좌우로 드래그 앤 드랍이 잘 작동되는 것을 확인할 수 있다.
다만, 좌우 요소가 다른 지역에게까지 영향을 주면서 순서를 뒤바꾸어버리는 것을 확인할 수 있다.


🛠 에러로그 : 아이템이 포함된 영역에서만 이벤트를 발생하도록 수정

 <div className="center-box">
   <DualSelector
      onDragStart={onDragStart}
      onDragEnter={onAvailableItemDragEnter}
      onDragOver={onDragOver}
      onDragEnd={onDragEnd}
	  id="leftSelector"
   />
   <DualSelector
      onDragStart={onDragStart}
      onDragEnter={onSelectedItemDragEnter}
      onDragOver={onDragOver}
      onDragEnd={onDragEnd}
      id="rightSelector"
   />
</div>

듀얼 셀렉터를 구현할때, 컴포넌트의 재사용성을 고려해서 한 컴포넌트를 2번 사용해서 구현했고, prop으로 데이터를 각기 다르게 내려줬다.

각 셀렉터에 문자열로 각기 다른 id값을 props로 전달해주고,
이벤트를 발생시키는 엘리먼트(ul)의 위치가 맞을때만 이벤트를 발생시킬수 있도록 코드를 추가해줬다.

const [draggingSectionId, setDraggingSectionId] = useState(null);// 1

const onDragStart = (e, index, id) => {
  draggingItemIndex.current = index;
  e.target.classList.add('grabbing');
  setDraggingSectionId(id); // 2

const onLeftItemDragEnter = (e, index) => {
  if (draggingSectionId === 'leftSelector') { // 3-1
    draggingOverItemIndex.current = index;
    const copyListItems = [...availableOptionsArr];
    const dragItemContent = copyListItems[draggingItemIndex.current];
    // 얕은 복사로 만든 카피 배열에서 드래깅되는 아이템을 하나 제거해주고
    copyListItems.splice(draggingItemIndex.current, 1);
    // 카피 리스트 배열에서 드레깅되는 아이템이 지나간 아이템의 인덱스에 드레그된 아이템을 추가해준다.
    copyListItems.splice(draggingOverItemIndex.current, 0, dragItemContent);
    // 드래깅된 아이템의 장소를 드래그 오버된 아이템의 인덱스로 바꾸어준다.
    draggingItemIndex.current = draggingOverItemIndex.current;
    // 드래그 오버 아이템의 useRef객체의 current 값을 초기화해준다.
    draggingOverItemIndex.current = null;
    // 리스트를 새롭게 랜더링할 수 있도록 상태를 업데이트해준다.
    setAvailableOptionsArr(copyListItems);
  }
};

const onRightItemDragEnter = (e, index) => {
  if (draggingSectionId === 'rightItemSelector') { // 3-2
    draggingOverItemIndex.current = index;
    const copyListItems = [...selectedOptionsArr];
    const dragItemContent = copyListItems[draggingItemIndex.current];
    // 얕은 복사로 만든 카피 배열에서 드래깅되는 아이템을 하나 제거해주고
    copyListItems.splice(draggingItemIndex.current, 1);
    // 카피 리스트 배열에서 드레깅되는 아이템이 지나간 아이템의 인덱스에 드레그된 아이템을 추가해준다.
    copyListItems.splice(draggingOverItemIndex.current, 0, dragItemContent);
    // 드래깅된 아이템의 장소를 드래그 오버된 아이템의 인덱스로 바꾸어준다.
    draggingItemIndex.current = draggingOverItemIndex.current;
    // 드래그 오버 아이템의 useRef객체의 current 값을 초기화해준다.
    draggingOverItemIndex.current = null;
    // 리스트를 새롭게 랜더링할 수 있도록 상태를 업데이트해준다.
    setSelectedOptionsArr(copyListItems);
  }
};
  
const onDragEnd = (e) => {
  e.target.classList.remove('grabbing');
  setDraggingSectionId(null); // 4
};
<ul className="select-list">
  {optionsArr.map((option, idx) => {
    return (
      <li
        key={idx}
        onClick={(e) => onClickHandler(e, idx)}
        onDragStart={(e) => onDragStart(e, idx, id)}
        onDragEnter={(e) => onDragEnter(e, idx)}
        onDragOver={onDragOver}
        onDragEnd={onDragEnd}
        draggable
	/>
   );
  })}
</ul>

1) 컴포넌트 단에서 id값을 관리할 수 있도록 state를 추가해주었다.

2) 드래그가 시작되었을때 발생하는 이벤트(onDragStart)가 발생했을때, 해당 엘리먼트의 id를 setState 함수를 발생시켜주었다.

3) if문을 통해 enter이벤트가 발생할때 id값을 비교해주고, 같을때만 발생시킨다.

4) 드래그가 종료되었을때 발생하는 이벤트(onDragEnd)에서 state에서 관리되는 id값을 null로 초기화시켜준다.


구현모습

좌우 다른 영역에서 발생한 이벤트가 다른 영역에서는 발생하지 않는다. 에러핸들링 성공 😙


마치며

기업 과제로 주어진 과제를 수행하는 것이기 때문에 react dnd와 같은 라이브러리를 사용하지 않고 코드를 구현했다.

라이브러리를 사용하면 아주 간단하게 구현할 수 있다.

하지만, 마우스 이벤트를 익히는 차원에서 구현해보는 것을 추천한다.


참고 링크

profile
안녕하세요 주니어 프론트엔드 개발자 양윤성입니다.

1개의 댓글

comment-user-thumbnail
2023년 2월 21일

터치이벤트로는 불가능할까요? 영안나오네여ㅠㅠ

답글 달기