Drag&Drop API로 스티커 보드 구현하기

둘러봐 기술블로그·2023년 9월 24일

Toy Project 🪀

목록 보기
2/4
post-thumbnail

결과물

시연

위의 webp 파일을 보면 알 수 있듯, OS에서 드래그를 통해 이미지 파일을 전달하고 스티커를 생성한다.

개발 동기

1. 모던 자바스크립트 공부

저번 반응속도 테스터처럼 모던 자바스크립트를 제대로 익히기 위해 이번 토이 프로젝트를 진행했다.

굳이 스티커 보드를 프로젝트의 주제로 선택한 이유는 ES6에 도입된 class와 HTML5에 추가된 drag & drop API에 관심이 있어서이다.

개발 과정

설계

구현하고 싶은 기능 열거하기

필자가 구현하고 싶은 기능과 요구사항은 아래와 같다

기능 1 : OS에 있는 이미지를 웹 페이지로 Drag & Drop하면, 스티커 생성

  • 스티커를 마우스 위치에 생성하여 배치도 동시에 해야 함
  • 이미지가 아닌 파일의 입력은 무시해야 함

기능 2 : 스티커를 Drag & Drop하면, 스티커 이동

  • 스티커가 스티커 보드 밖으로 나갈 수 없도록 해야 함

기능 3 : 스티커를 쓰레기통으로 Drag & Drop하면, 스티커 삭제

  • 스티커가 쓰레기통 위에 드래그되면 삭제 영역의 색깔이 변해야 함

위의 기능 서술에서 등장하는 조건과 객체에 주목해보자. 조건은 Drag & Drop으로 발생하는 이벤트이고, 객체는 스티커(Sticker)스티커 보드(StickerBoard), 그리고 쓰레기통(Wastebasket)이다. 따라서 이 세가지 객체를 클래스로 정의하고, 이벤트 발생 시 이 클래스들 간의 상호작용을 통해 위의 기능을 구현하면 된다.

구현

HTML

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toy Project : Sticker Board</title>
</head>
<body>
  <sticker-board>
      Drag image here!
  </sticker-board>
  <waste-basket>
      Remove
  </waste-basket>
</body>

CSS

sticker-board {
    width : 100%; 
    background-color : #1b1b1b;
    color : white;
    text-align : center;
    line-height : 280px;
    font-size : 2em;
}

waste-basket {
    width : 100%; 
    background-color : #f50056;
    color : white;
    text-align : center;
    line-height : 50px;
    font-size : 2em;
}

waste-basket.dragEnter {
    background-color : #f76a9c;
}

JavaScript

global 변수 선언
let dragged, pageX, pageY;
let STICKER_MAX_WIDTH = 120,
    STICKER_MAX_HEIGHT = 120;
Sticker 구현
class Sticker extends HTMLImageElement{
    constructor(){
        super();
        this.style.position = "absolute";
        
        this.style.maxWidth = STICKER_MAX_WIDTH + "px";
        this.style.maxHeight = STICKER_MAX_HEIGHT + "px";

        this.addEventListener("drag", this.drag);
        this.addEventListener("dragstart", this.dragStart);
        this.addEventListener("dragend", this.dragEnd);
        this.addEventListener("load", this.onLoad);
    }
    dragStart(e){
        this.style.opacity = "0%";
        this.leftOnImg = e.clientX - this.getBoundingClientRect().left;
        this.topOnImg = e.clientY - this.getBoundingClientRect().top;
        dragged = this;
    }
    drag(e){
        e.preventDefault();
    }
    dragEnd(e){
        e.preventDefault();
        
        this.style.opacity = "100%";
        this.style.left = Math.max(Math.min(e.pageX - this.leftOnImg, this.parentRight - this.clientWidth), this.parentLeft) + "px";
        this.style.top = Math.max(Math.min(e.pageY - this.topOnImg, this.parentBottom - this.clientHeight), this.parentTop) + "px";
    }
    onLoad(e){
        URL.revokeObjectURL(this.src);
        this.setParentRect();
        this.style.left = Math.max(Math.min(pageX - this.clientWidth/2, this.parentRight - STICKER_MAX_WIDTH), this.parentLeft) + "px";
        this.style.top = Math.max(Math.min(pageY - this.clientHeight, this.parentBottom - STICKER_MAX_HEIGHT), this.parentTop) + "px";
    }
    setParentRect(){
        this.parentRight = this.parentElement.rect.right;
        this.parentBottom = this.parentElement.rect.bottom;
        this.parentLeft = this.parentElement.rect.left;
        this.parentTop = this.parentElement.rect.top;
    }
    moveTo(x, y){
        
    }
}
customElements.define("sticker-img", Sticker, { extends : "img" });
StickerBoard 구현
class StickerBoard extends HTMLElement{
    constructor(){
        super();
        this.style.display = "block";
        this.rect = this.getBoundingClientRect();
        this.addEventListener("drop", this.drop);
        this.addEventListener("dragover", this.dragOver);
    }
    drop(e){
        e.preventDefault();
        e.stopPropagation();
        const item = e.dataTransfer.items[0];
        if (item.kind === "file" && /^image*/.test(item.type)) {
            let src = URL.createObjectURL(item.getAsFile());
            let sticker = document.createElement("img", {is : "sticker-img"});
            pageX = e.pageX, pageY = e.pageY;
            sticker.src = src;

            this.appendChild(sticker);
        }
    }
    dragOver(e){
        e.preventDefault();
    }
}
customElements.define("sticker-board", StickerBoard);
Wastebasket 구현
class Wastebasket extends HTMLElement {
    constructor() {
        super();
        this.style.display = "block";
      
        this.addEventListener("drop", this.drop);
        this.addEventListener("dragover", this.dragOver);
        this.addEventListener("dragleave", this.dragLeave);
        this.addEventListener("dragenter", this.dragEnter);
    }
    drop(e){
        e.preventDefault();
        dragged.remove();
        this.classList.remove("dragEnter");
    }
    dragOver(e){
        e.preventDefault();
    }
    dragLeave(e){
        this.classList.remove("dragEnter");
    }
    dragEnter(e){
        this.classList.add("dragEnter");
    }
}
customElements.define("waste-basket", Wastebasket);

개발 후기

이제 JavaScript로 OOP를 제대로 할 수 있어서 좋았다

ES6에서 클래스가 추가되기 전인 ES5에서는 클래스를 function을 통해 생성했고, 상속을 prototype 속성을 통해서 구현했다. 이 방법은 해본 사람들은 알겠지만 뭔가 짜친다. 이제 class로 제대로 OOP를 할 수 있어서 좋았다.

개발 과정에서 기록을 많이 남겨야 겠다

이번 토이 프로젝트를 하면서 글로 남기고 싶은 부분이 많았다. 그런데 정작 구현이 끝난 뒤에 글을 쓰려고 하니 내가 무엇을 글로 남기고 싶었는지 기억이 잘 안 난다...앞으로는 기록을 남기면서 개발을 해야 겠다.

profile
move out to : https://lobster-tech.com?utm_source=velog

1개의 댓글

comment-user-thumbnail
2025년 3월 2일

좋은 예시 잘보고 갑니다:D

답글 달기