방망이 깎는 코모

리액트로 드래그 앤 드롭 구현하는 방법 본문

개념정리

리액트로 드래그 앤 드롭 구현하는 방법

코모@kosmosticlay 2024. 2. 5. 15:39

 

https://bevacqua.github.io/dragula/

 

 

 

Dragula는 웹 애플리케이션에서 드래그 앤 드랍 기능을 쉽게 구현할 수 있도록 도와주는 자바스크립트 라이브러리다. Drag and drop so simple it hurts 라는 슬로건처럼, Dragula API를 이용하여 DOM 요소들 간의 드래그 앤 드랍 기능을 매우 간단하고 손쉽게 만들 수 있다.

Dragula 설치

npm을 통해 애플리케이션에 Dragula를 설치한다. 

npm install react-dragula --save

 

(옵션) Dragula CSS

Dragula가 정상적으로 작동하기 위해 지켜져야 할 CSS 스타일들이 있다. 총 4개의 CSS 클래스를 사용하며 해당 규칙은 아래와 같다. 자세한 CSS 규칙은 dist/dragula.css에서 확인 가능하다.

 

CSS 클래스 설명
gu-unselectable 드래깅 할 때 mirrorContainer 요소에 추가;
드래깅 하는 동안 mirrorContainer를 스타일링 하는데 사용
gu-transit 드래깅 될 때 원본 요소에 추가;
투명도(opacity)만 조절
gu-mirror 미러 이미지에 추가;
(미러 이미지는 초기 컨테이너가 아닌 mirrorContainer에 추가됨)
gu-hide display:none을 적용하기 위한 helper 클래스

Dragula 기본 사용 방법

JavaScript는 직접 DOM요소를 선택하고, Dragula() 함수를 호출하여 생성된 drake 객체에게 전달된다. 객체가 제공하는 이벤트와 메서드등을 사용하여 드래그 앤 드랍 기능을 구현할 수 있다. 

 

반면, 리액트에서는 직접 DOM을 선택하지 않고 useRef 훅을 사용하여 DOM요소에 접근한다. 그리고 컴포넌트가 마운트 될 때, useEffect 훅 내에서 Dragula를 초기화 하고, refDragula() 함수에 전달하여 드래그 앤 드롭 기능을 활성화시킨다. 

 

Dragula(ref, options);

 

두 번째 매개변수인 options는 드래그 앤 드롭 동작을 미세하게 조정하는데 사용되는 설정이다. 그 중 revertOnSpill은 드래그 중인 요소를 드랍할 수 없는 위치에 놓았을 때, 시작 위치로 돌아갈지(true), 현재 위치에 남을지(false) 결정한다. removeOnSpill는 드래그 중인 요소를 유효하지 않은 위치에 놓았을 때, 제거될 지 여부를 결정한다. true일 경우 DOM에서 제거되고, false일 경우 revertOnSpill에서 설정한 방식으로 돌아간다.

(참고)

  • 컨테이너 밖에 드래그하면 요소 제거 : revertOnSpill: false + removeOnSpill: true
  • 컨테이너 밖에 드래그해도 요소 원래 위치로 복귀 : revertOnSpill: true + removeOnSpill: false

(이론) 리액트에서 Dragula를 사용하는 방법

클래스형 컴포넌트 방식

아래는 (공식문서에서 제공하는) 클래스 컴포넌트 방식으로 Dragula 기능을 구현하는 방식이다.

import * as React from "react";
import * as ReactDOM from 'react-dom';
import Dragula from 'react-dragula';

export class App extends React.Component {
  render () {
    return <div className='container' ref={this.dragulaDecorator}>
      <div>Swap me around</div>
      <div>Swap her around</div>
      <div>Swap him around</div>
      <div>Swap them around</div>
      <div>Swap us around</div>
      <div>Swap things around</div>
      <div>Swap everything around</div>
    </div>;
  },
  
  dragulaDecorator = (componentBackingInstance) => {
    if (componentBackingInstance) {
      let options = { };
      Dragula([componentBackingInstance], options);
    }
  };
});

ReactDOM.render(<App />, document.getElementById('examples'));

 

함수형 컴포넌트 방식

Dragula 공식문서에서는 클래스형 컴포넌트 방식의 사용 방법만 안내했지만, 개인적으로 함수형 컴포넌트 방식을 선호하기 때문에 코드를 수정해보았다. 클래스형 컴포넌트의 dragulaDecorator 함수는 useRef와 useEffect 훅을 사용하여 함수형 컴포넌트에서도 구현할 수 있다. 

 

useRef 훅을 사용하여 참조(ref)를 생성하는데, 생성된 참조의 초기값으로 null을 전달한다. 왜냐하면 컴포넌트가 마운트되기 전에는 참조하려는 DOM요소가 존재하지 않기 때문이다. 컴포넌트가 마운트된 후, 참조(ref)는 ref 속성을 통해서 실제 DOM요소를 .current 프로퍼티에 할당한다.

import React, { useRef, useEffect } from 'react';
import ReactDOM from 'react-dom';
import Dragula from 'react-dragula';

function App() {
  const containerRef = useRef(null);

  useEffect(() => {
    if (containerRef.current) {
      Dragula([containerRef.current]);
    }

    return () => {
    // 컴포넌트가 unmount시 실행될 cleanup함수 추가
    };
  }, []);

  return (
    <div className='container' ref={containerRef}>
      <div>Swap me around</div>
      <div>Swap her around</div>
      <div>Swap him around</div>
      <div>Swap them around</div>
      <div>Swap us around</div>
      <div>Swap things around</div>
      <div>Swap everything around</div>
    </div>
  );
}

ReactDOM.render(<App />, document.getElementById('examples'));

(실습) 리액트에서 Dragula를 사용하는 방법

Dragula와 리액트를 이용한 드래그 앤 드롭 기능 구현 화면

 

useState를 이용한 상태 관리

할 일 목록(todoItems), 입력 값(inputValue), 카테고리 값(categoryValue), 카테고리 이름(categoryName)을 useState 훅을 이용하여 상태를 추적하고 업데이트하였다.

  const [todoItems, setTodoItems] = useState([]);
  const [inputValue, setInputValue] = useState("");
  const [categoryValue, setCategoryValue] = useState("0");
  const [categoryName, setCategoryName] = useState("");

 

useRef를 이용한 DOM요소 접근

Dragula 컨테이너로 사용될 3개의 <ul> DOM요소를 useRef를 통해 각각 참조한다. 다만 컴포넌트가 마운트 되기 전에는 해당 DOM요소가 존재하지 않으므로, null을 초기값으로 설정하였다.

  const todoOptionsRef = useRef(null);
  const todoTodayRef = useRef(null);
  const todoRemovedRef = useRef(null);

 

useEffect를 이용한 동작 설정

컴포넌트가 마운트된 후 Dragula를 설정하고 초기화 한다. 이 과정에서 useRef로 참조된 <ul> 요소들이 'Dragula 컨테이너'로써 등록된다. 의존성 배열을 빈 배열로 전달함으로써 Dragula는 처음 컴포넌트가 마운트 될 때만 실행하도록 설정하였다. 그리고 간단한 기능 구현이기 때문에 유효성 검사를 위한 if문은 생략하였다.

  useEffect(() => {
    const containers = [
      todoOptionsRef.current,
      todoTodayRef.current,
      todoRemovedRef.current,
    ];

    let options = {};
    Dragula(containers, options);
  }, []);

완성 화면

📌 깃허브 레포지토리 참고

 


마치며

Dragula 라이브러리를 이용하여 JavaScript로 '드래그 앤 드롭' 기능을 구현하는 방법을 학습했다. Dragula의 So simple it hurts 라는 슬로건처럼, 그 과정이 너무 간단해서 허탈하기까지 했다. 그래서 이제 막 배우기 시작한 리액트도 실습할 겸 해당 기능을 리액트로 재 구현해보았다.   

 

간단한 기능이었지만, 리액트 개념 중 useState와 useEffect만 배우고 시작한 상태라, 내게 추가적으로 필요한 훅이 useRef 라는 것을 알기까지 시간이 좀 걸렸다. 더이상 document.querySelector()를 쓸 필요가 없다는 사실은 아직도 믿기지 않는다. 

 

다음에는 '휴지통' 목록에 아이템이 없을 때는 '전체 삭제(아이콘)'이 제거되고, 아이템이 하나라도 추가되면 등장할 수 있도록 수정해보려고 한다. 사실 해당 기능을 구현해보려고 시도했으나, useRef를 통해 DOM 요소를 받아서 실시간으로 상태관리를 해야하는 부분에서 생각처럼 동작하고 있지 않다. 그리고 중간에 코드를 수정하느라 카테고리 값(categoryValue) 로직도 비효율적으로 설계되어 이 부분도 함께 수정할 필요가 있다. 아직 리액트 훅에 대해 기초적인 개념 밖에 없다보니 추가적인 학습 후에 수정할 생각이다.

 


참고 문헌

1. Dragula, Official React wrapper for dragula : https://github.com/bevacqua/react-dragula?tab=readme-ov-file

2. Dragula, Official Document : https://github.com/bevacqua/dragula?tab=readme-ov-file

 

Comments