본문 바로가기
dev/javascript

[javascript] 자바스크립트 드래그 앤 드롭 draggable

by 최연탄 2022. 9. 20.
728x90
반응형

참고: https://www.javascripttutorial.net/web-apis/javascript-drag-and-drop/

이 튜토리얼에서는 자바스크립트 드래그 앤 드롭 api와 간단한 드래그 앤 드롭 앱을 구현하는 방법에 대해 알아보겠습니다.

자바스크립트의 드래그 앤 드롭 API

HTML5는 공식적으로 드래그 앤 드롭 스팩을 공개했습니다. 대부분의 최신 웹 브라우저는 HTML5 스팩을 기반으로 네이티브 드래그 앤 드롭 기능을 구현했습니다.

이 드래그 앤 드롭 기능은 기본적으로 이미지와 텍스트만 끌 수 있습니다. 이미지를 끌려면 마우스 버튼을 누른 상태에서 이동하기만 하면 됩니다. 텍스트를 끌려면 일부 텍스트를 하이라이트 해놓고 이미지를 끌 때와 같은 방식으로 끌어다 놓아야 합니다.

HTML5 스팩은 거의 모든 요소를 드래그할 수 있도록 지정합니다. 요소를 끌 수 있게 하려면 HTML 태그에 draggable=true 속성을 넣으면 됩니다. 예를 들면

<div class="item" draggable="true"></div>

draggable 항목의 이벤트

항목을 드래그 하면 다음의 이벤트들이 순서대로 실행됩니다.

  • dragstart
  • drag
  • dragend

마우스 버튼을 누르고 마우스를 움직이기 시작하면 지금 드래고 하고있는 요소에 dragstart 이벤트가 시작됩니다. 이때 커서는 no-drop(원안에 대각선 선) 심볼로 바뀌어서 자기 자신에게 드롭할 수 없다고 표시합니다.

dragstart 이벤트가 발생한 후 요소를 끌고 있는 동안에는 drag 이벤트가 반복적으로 발생합니다. 여기서 끌기를 중단하면 dragend 이벤트가 실행됩니다. 이벤트 콜백 함수의 매개변수로 들어오는 target(event.target)은 현재 드래그되고 있는 대상입니다.

기본적으로 브라우저는 끌어온 요소의 모양을 변경하지 않습니다. 그러므로 드래그 시 모양을 바꾸고 싶다면 환경설정을 사용자 정의해야 합니다.

drop target에 대한 이벤트

올바른 drop 영역으로 항목을 드래그 할 때에는 다음의 이벤트들이 순서대로 실행됩니다.

  • dragenter
  • dragover
  • dragleave 또는 drop

dragenter 이벤트는 드래그하는 항목이 drop 영역에 들어왔을 때 실행됩니다. dragenter가 실행된 다음에 항목을 계속 drop 영역안에서 움직이면 dragover가 반복적으로 실행됩니다. 이때 항목을 drop 영역 밖으로 드래그하면 dragover 이벤트 실행이 중지되고 dragleave 이벤트가 실행됩니다. 여기서 항목을 drop 영역 밖으로 끌지 않고 drop 영역 안에서 놓으면 drop 이벤트가 실행됩니다.

dragenter, dragover, dragleave, drop 이벤트의 target(event.target)은 드롭 영역에 대한 항목입니다.

유효한 드롭 타겟

거의 대부분의 항목은 드롭 타겟 이벤트(dragenter, dragover, dragleave, drop)를 받을 수 있습니다. 하지만 기본 값으로 드롭을 허용하는 것은 아닙니다. 만일 드롭을 허용하지 않는 항목에 드롭을 할 경우 drop 이벤트가 실행되지 않습니다.

HTML 항목을 유효한 드롭 타겟으로 만드려면 이벤트 핸들러에서 event.preventDefault() 메소드를 호출하여 dragenter, dragover 이벤트의 기본 동작을 오버라이드할 수 있습니다. 자세한 정보는 뒤에 나오는 예제에서 확인할 수 있습니다.

dataTransfer 객체를 활용한 자료 전송

드래그 앤 드롭시 자료를 전달하기 위해서는 dataTransfer 객체를 사용해야합니다. dataTransfer 객체는 이벤트의 속성으로 드래그한 항목에서 드롭 타겟으로 자료를 전송할 수 있습니다.

dataTransfer 객체는 setData(), getData() 두개의 메소드를 가지고있습니다.

setData() 메소드를 사용하여 드래그할 때에 원하는 데이터를 특정한 포멧으로 지정할 수 있습니다.

dataTransfer.setData(format, data)

여기서 format은 text/plain 이나 text/uri-list가 될 수 있습니다. data 매개변수에는 드래그 객체에 추가할 문자열 자료입니다.

getData() 메소드는 setData() 메소드로 저장한 드래그 값을 가져옵니다. 이는 하나의 매개변수를 가집니다.

dataTransfer.getData(format)

format은 text/plain 이나 text/uri-list이고 리턴값은 setData() 메소드로 저장했던 문자열 값 입니다.

자바스크립트 드래그 앤 드롭 예제

이제 드래그 앤 드롭 API를 사용한 간단한 앱을 만들어 보겠습니다.

프로젝트 구조 만들기

먼저 drag-n-drop-basics라는 디렉토리를 만듭니다. 이 디렉토리 안에 css와 js 디렉토리도 만듭니다.

두 번째로 js 디렉토리에 app.js 파일을 만들고, css 디렉토리에 style.css 파일을 만듭니다. 그리고 drag-n-drop-basics 밑에는 index.html을 만듭니다.

마지막으로 아래 예제 처럼 index.html 파일 헤더에 style.css를 링크하고 script 태그로 app.js를 링크합니다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JavaScript - Drag and Drop Demo</title>
    <link rel="stylesheet" href="css/style.css">
</head>

<body>
    <script src="js/app.js"></script>
</body>

</html>

style.css 파일에는 다음의 코드를 입력합니다.

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    font-size: 16px;
    background-color: #fff;
    overflow: hidden;
}

h1 {
    color: #323330;
}

.container {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    margin: 20px;

}

.drop-targets {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;

    margin: 20px 0;
}

.box {
    height: 150px;
    width: 150px;
    border: solid 3px #ccc;
    margin: 10px;

    /* align items in the box */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

}


.drag-over {
    border: dashed 3px red;
}

.item {
    height: 75px;
    width: 75px;
    background-color: #F0DB4F;

}

.hide {
    display: none;
}

index.html 만들기

다음의 코드를 index.html에 입력합니다.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JavaScript - Drag and Drop Demo</title>
    <link rel="stylesheet" href="css/style.css">
</head>

<body>
    <div class="container">
        <h1>JavaScript - Drag and Drop</h1>
        <div class="drop-targets">
            <div class="box">
                <div class="item" id="item">
                </div>
            </div>
            <div class="box"></div>
            <div class="box"></div>
        </div>
    </div>
    <script src="js/app.js"></script>
</body>

</html>

이 index.html에서는 .container로 지정한 항목을 사용해 .drop-targets을 정렬하고 .drop-targets 항목 안에는 클래스 명이 .box인 항목을 세개 놓고 첫 번째 .box 안에는 .item 이라는 항목을 추가했습니다.

위 코드를 저장하고 index.html을 실행하면 다음 그림과 같은 화면이 나타나는 것을 확인할 수 있습니다. 하지만 노란 박스를 드래그할 수는 없습니다.

노란 박스를 드래그할 수 있게 하려면 다음의 예제와 같이 draggable=true 항목을 추가해야합니다.

<div class="item" id="item" draggable="true">

이제 index.html을 브라우저로 다시 열어보면 노란 박스가 드래그 되는 것을 확인할 수 있습니다.

드래그 가능한 항목의 이벤트 제어하기

이전에 style.css 파일에 항목을 숨길 수 있도록 .hide 클래스를 정의 했습니다.

.hide {
    display: none;
}

이제 app.js 파일에 다음 코드를 추가합니다.

// dom에서 item 항목 가져오기
const item = document.querySelector('.item');

// dragstart 이벤트 추가
item.addEventListener('dragstart', dragStart);

// dragstart 제어
function dragStart(e) {
   console.log('drag starts...');
}

코드 설명:

  • querySelector() 메소드를 사용해 dom에서 드래그 가능한 항목을 가져옵니다.
  • 가져온 항목에 dragstart 이벤트 핸들러를 추가합니다.
  • 이벤트 핸들러로 지정된 dragStart() 함수를 정의합니다.

이제 index.html을 새로 열어 노란 박스를 드래그 하면 콘솔(크롬에서는 F12누른 후 console 탭 클릭)에 "drag starts..." 라는 메시지가 뜨는 것을 확인할 수 있습니다.

이제 dragStart() 이벤트 핸들 함수에서 드래그 중인 항목의 id를 저장하고 숨기도록 합니다. 다음 예제를 참고하세요.

function dragStart(e) {
    e.dataTransfer.setData('text/plain', e.target.id);
    e.target.classList.add('hide');
}

이 상태로 화면을 갱신하면 노란박스 드래그 시 원래 자리에 있던 노란박스와 드래그 중인 노란박스가 모두 사라지는 것을 확인할 수 있습니다. 이런걸 원한게 아니고 원래 박스는 사라지고 드래그 중인 박스는 보이게 하고싶습니다. 그렇다면 코드를 다음과 같이 수정합니다.

function dragStart(e) {
    e.dataTransfer.setData('text/plain', e.target.id);
    setTimeout(() => {
        e.target.classList.add('hide');
    }, 0);
}

이제 index.html을 다시 실행해보면 드래그 중이 항목만 보이는 것을 확인할 수 있습니다.

드롭 타겟의 이벤트 제어하기

앞서 style.css에 .drag-over 클래스를 정의해 드래그 객체가 드롭 타겟 안에 들어왔을 때 점선으로 바뀌도록하였습니다.

.drag-over {
    border: dashed 3px red;
}

이제 다음 예제와 같이 app.js에 드롭 타겟에 대한 dragenter, dragover, dragleave, drop 이벤트 핸들러를 넣어야합니다.

const boxes = document.querySelectorAll('.box');

boxes.forEach(box => {
    box.addEventListener('dragenter', dragEnter)
    box.addEventListener('dragover', dragOver);
    box.addEventListener('dragleave', dragLeave);
    box.addEventListener('drop', drop);
});

function dragEnter(e) {
}

function dragOver(e) {
}

function dragLeave(e) {
}

function drop(e) {
}

드롭 타겟이 점선으로 바뀌는 시점은 dragenter와 dragover 이벤트가 실행되었을 때 입니다. 또한 dragleave, drop 이벤트가 실행되었다면 다시 원래대로 돌아가야합니다. 이를 위해서 다음 예제처럼 .drag-over 클래스를 드롭 타겟에 추가/제거하는 코드를 넣어야 합니다.

function dragEnter(e) {
    e.target.classList.add('drag-over');
}

function dragOver(e) {
    e.target.classList.add('drag-over');
}

function dragLeave(e) {
    e.target.classList.remove('drag-over');
}

function drop(e) {
    e.target.classList.remove('drag-over');
}

이제 노란박스를 드래그해서 드롭 타겟 위로 끌어보면 드롭 타겟이 점선으로 바뀌는 것을 확인할 수 있습니다.

드롭 타겟을 유효하게 만들려면 dragenter, dragover 이벤트 핸들러에 event.preventDefault()를 실행해야 합니다.

function dragEnter(e) {
    e.preventDefault();
    e.target.classList.add('drag-over');
}

function dragOver(e) {
    e.preventDefault();
    e.target.classList.add('drag-over');
}

위와 같이 실행하지 않는다면 drop 이벤트는 절대 실행되지 않습니다. 왜냐하면 div 항목의 기본값은 유효한 드롭 타겟이 아니기 때문입니다.

이제 드롭 타겟에 드래그 항목을 넣으면 드래그 했던 노란박스가 바로 사라지는 것을 볼 수 있습니다. 이를 해결하기 위해서는 drop 이벤트 핸들러를 정의해야 합니다.

  • 먼저 dataTransfer 객체의 getData() 메소드를 통해 드래그 중인 항목의 id를 가져와야합니다.
  • 그 다음 드래그 중인 항목을 드롭 타겟 항목의 자식으로 추가해야합니다.
  • 마지막으로 .hide 클래스를 지워 드래그한 항목이 화면에 보이도록 합니다.

다음은 drop 이벤트 핸들러의 전체 코드입니다.

function drop(e) {
    e.target.classList.remove('drag-over');

    // 드래그 중인 항목의 id 가져오기
    const id = e.dataTransfer.getData('text/plain');
    const draggable = document.getElementById(id);

    // 드롭 타겟에 드래그 항목 추가
    e.target.appendChild(draggable);

    // 드래그 했던 항목 화면에 보이기
    draggable.classList.remove('hide');
}

이제 다시 실행해 보면 드래그 앤 드롭이 원하는 대로 작동하는 것을 확인할 수 있습니다.

전체 코드

- index.html

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>JavaScript - Drag and Drop Demo</title>
    <link rel="stylesheet" href="css/style.css">
</head>

<body>
    <div class="container">
        <h1>JavaScript - Drag and Drop</h1>
        <div class="drop-targets">
            <div class="box">
                <div class="item" id="item" draggable="true">
                </div>
            </div>
            <div class="box"></div>
            <div class="box"></div>
        </div>
    </div>
    <script src="js/app.js"></script>
</body>

</html>

- style.css

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
    font-size: 16px;
    background-color: #fff;
    overflow: hidden;
}

h1 {
    color: #323330;
}

.container {
    height: 100vh;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    margin: 20px;

}

.drop-targets {
    display: flex;
    flex-direction: row;
    justify-content: space-around;
    align-items: center;

    margin: 20px 0;
}

.box {
    height: 150px;
    width: 150px;
    border: solid 3px #ccc;
    margin: 10px;

    /* align items in the box */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;

}


.drag-over {
    border: dashed 3px red;
}

.item {
    height: 75px;
    width: 75px;
    background-color: #F0DB4F;

}

.hide {
    display: none;
}

- app.js

const item = document.querySelector('.item');
const boxes = document.querySelectorAll('.box');

item.addEventListener('dragstart', dragStart);

boxes.forEach(box => {
    box.addEventListener('dragenter', dragEnter)
    box.addEventListener('dragover', dragOver);
    box.addEventListener('dragleave', dragLeave);
    box.addEventListener('drop', drop);
});

function dragStart(e) {
    e.dataTransfer.setData('text/plain', e.target.id);

    setTimeout(() => {
        e.target.classList.add('hide');
    }, 0);
}

function dragEnter(e) {
    e.preventDefault();
    e.target.classList.add('drag-over');
}

function dragOver(e) {
    e.preventDefault();
    e.target.classList.add('drag-over');
}

function dragLeave(e) {
    e.target.classList.remove('drag-over');
}

function drop(e) {
    e.target.classList.remove('drag-over');

    const id = e.dataTransfer.getData('text/plain');
    const draggable = document.getElementById(id);

    e.target.appendChild(draggable);

    draggable.classList.remove('hide');
}

정리

  • draggable 속성과 true 값을 할당하여 항목을 끌 수 있도록 합니다.
  • dragstart, drag, dragend 이벤트는 드래그 가능한 항목에서 발생합니다.
  • dragenter, dragover, dragleave, drop 이벤트는 드롭 타겟에서 발생합니다.
  • 항목을 유효한 드롭 대상으로 만들려면 dragenter와 dragover 이벤트 핸들러에서 event.preventDefault()를 호출합니다.
  • setData() 및 getData() 메소드와 함께 event.dataTransfer 객체를 사용하여 드래그 앤 드롭 작업에서 데이터를 전송합니다.

관련 글

자바스크립트 jQuery-ui 드래그 앤 드롭

반응형

댓글