드래그 드랍 페이지 구현하기

Drag 를 통해서 특정한 아이콘을 특정 필드 내에 배치하는 기법을 적용하고 싶습니다. 

마치 워드프레스의 Elementor 편집기와 비슷한 로직을 구현하기를 원하는 거죠.

이를 위해서 어떤 식으로 필드를 구성하면 좋을지 정리해보겠습니다.

배치

우선은 왼쪽에 특정한 아이콘들이 있는 Side Bar가 있고,

여기서 아이콘을 드래그해서, 특정한 필드를 생성할 수 있는 메인 구역을 둘 예정입니다.

코드를 다음과 같이 우선 구성해보죠.

 

				
					import React, { useState } from "react";
import {
  MdCabin,
  MdAbc,
  MdBackpack,
  MdSpaceBar,
  MdKayaking,
  MdBarChart,
} from "react-icons/md";
import "./Style.css";

//아이콘이 들어있는 사이드 메뉴
const SideBar = () => {
  return (
    <div className="SideMenu">
      <div className="item-line">
        <MdCabin size={60} />
        <MdAbc size={60} />
      </div>
      <div className="item-line">
        <MdBackpack size={60} />
        <MdSpaceBar size={60} />
      </div>
      <div className="item-line">
        <MdKayaking size={60} />
        <MdBarChart size={60} />
      </div>
    </div>
  );
};

//아이콘을 드래그 하여 특정한 필드를 생성하기 위한 공간
const MainBuilder = () => {
  return (
    <div className="MainBuilder">
      <div className="DragField">이곳에 아이콘을 드래그 할 것이다.</div>
    </div>
  );
};

const Page = () => {
  return (
    <div className="page">
      <SideBar />
      <MainBuilder />
    </div>
  );
};

export default Page;

				
			
				
					
.SideMenu{
    width: 200px;
    height: 100%;
    display: flex;
    flex-direction: column;
    background-color: aliceblue;
}

.item-line{
    margin-top: 15px;
margin-left: auto;
margin-right: auto;
}


.MainBuilder{
    width: 800px;
    min-height: 900px;
    flex-direction: column;
}

.DragField{
    display: flex;
    background-color: aquamarine;
    justify-content: center;
    margin: auto;
    width: 500px;
    height: 600px;
}





.page{
    display: flex;
    margin: auto;
    height: 900px;
}

				
			

위와 같이 코드를 우선 크게 코드를 구성했습니다.

왼쪽에는 아이콘이 2개씩 줄지어진 사이드 메뉴가.

오른쪽 공간에는 아이콘을 드래그하여 특정한 필드를 생성하기 위한 공간을 배치했습니다.

실제로 위 코드를 통해 만들어진 페이지를 보면…

위와 같은 느낌이 됩니다.

아이콘은 그냥 아무거나 가지고 왔습니다.

저 아이콘을 이제 초록색 공간으로 드래그하여 특정한 요소를 만들 겁니다.

드래그를 위한 변경점 1

				
					      <div className="item-line">
        <div draggable="true">
          <MdCabin size={60} />
        </div>
        <div draggable="true">
          <MdAbc size={60} />
        </div>
      </div>
				
			

위에서 왼쪽에 아이콘을 2개씩 배치하기 위해 사용했던 부분을 위처럼 고쳐줍시다.

단순히 각각의 아이콘을 <div draggable=”true”> 내부로 넣어준 것입니다.

이렇게 되면 드래그가 가능합니다.

그럼 이제 위처럼 마우스로 드래그를 했을 때 커서에 잔상이 딸려오는 것을 볼 수 있습니다.

다만, 마우스 커서가 포인터로 변경이 되지 않으니까. 아이콘을 감싼 div 에 class 를 더해줍시다.

				
					      <div className="item-line">
        <div className="item-icon" draggable="true">
          <MdCabin size={60} />
        </div>
        <div className="item-icon" draggable="true">
          <MdAbc size={60} />
        </div>
      </div>
				
			
				
					.item-icon{
    cursor: pointer;
}
				
			

위처럼 변경을 해주면, 

아이콘 부분에 마우스 가져다대면 포인터가 생성이 되면서 끌어오는 것이 가능해집니다.

고럼 이제 아이콘을 저쪽으로 드래그해서 필드를 생성해봅시다.

드래그를 위한 변경점 2

				
					        <div
          className="item-icon"
          draggable="true"
          onDragStart={dragStartHandler}
          onDragEnd={dragEndHandler}
          targetType="A"
        >
          <MdCabin size={60} />
        </div>
				
			

위쪽에서 div 내부에 아이콘을 배치했죠?

이제는 이 div 에 위와 같은 식으로 함수를 배치할 겁니다.

보시면 targetType 이라는 값을 div 내에 넣은 것을 볼 수 있습니다. 

이 값에 따라서 생성되는 div 의 스타일을 조정하겠습니다. 

onDragStart 는 말 그대로 드래그를 시작할 때 작동하는 함수이고, 

onDragEnd 는 말 그대로 드래그를 끝냈을 때 작동하는 함수가 될 겁니다.

그럼 어떤 함수를 넣을 건지 봅시다. 

				
					//아이콘 드래그 시작시 위치 정보를 저장하는 함수
const dragStartHandler = (e) => {
  e.dataTransfer.setData("text/plain", null); // 필수: Firefox에서 드래그를 활성화하기 위함
  e.dataTransfer.setDragImage(e.target, 0, 0); // 드래그 이미지 설정

  // 드래그 시작 위치 저장
  e.target.setAttribute("data-start-x", e.clientX);
  e.target.setAttribute("data-start-y", e.clientY);

  // 요소의 원래 위치 저장
  e.target.setAttribute("data-original-x", e.target.offsetLeft);
  e.target.setAttribute("data-original-y", e.target.offsetTop);
};
				
			

우선은 아이콘을 드래그 하면 발동되는 함수입니다.

e.dataTransfer 은 드래그를 하는 동안에만 발동이 되는 메소드입니다.

중요한 건, 드래그를 하기 위해 선택한 아이콘의 위치를 기억하기 위해 사용하는 함수입니다. 

				
					//아이콘을 드래그에서 놓았을 때 작동하는 함수
const dragEndHandler = (e) => {
  const dragField = document.querySelector(".DragField");
  const box = dragField.getBoundingClientRect();
  const isInDragField =
    e.clientX >= box.left &&
    e.clientX <= box.right &&
    e.clientY >= box.top &&
    e.clientY <= box.bottom;

  if (isInDragField) {
    const targetType = e.target.getAttribute("targetType");
    const newElement = document.createElement("div");

    if (targetType === "A") {
      newElement.style.width = "100px";
      newElement.style.height = "100px";
      newElement.style.backgroundColor = "red";
    } else if (targetType === "B") {
      newElement.style.width = "150px";
      newElement.style.height = "150px";
      newElement.style.backgroundColor = "blue";
    }

    newElement.innerText = targetType;
    dragField.appendChild(newElement);
  }

  // 드래그된 요소를 원래 위치로 되돌림
  e.target.style.left = `${e.target.getAttribute("data-original-x")}px`;
  e.target.style.top = `${e.target.getAttribute("data-original-y")}px`;
};
				
			

다음은 아이콘을 드래그 하고 있다가 놓았을 때 발동되는 함수입니다.

여기서는 querySelector 을 통해서 특정한 Class 를 가지고 있는 요소를 찾아내어 dragField 라는 변수에 할당합니다.

이 dragField 뒤에 .getBoundingClientReact() 라는 메소드를 넣어주고 이를 box 라는 변수에 할당합니다.

그럼 이 box 의 위치를 알 수 있는데요.

isInDragField 라는 변수를 보면, 사용자가 드래그 하여 이동하고 있는 마우스의 위치를 X, Y 로 체크하고 있습니다.

Box 안쪽에 마우스가 있다면 True 가, 그렇지 않다면 False 가 반환이 될 겁니다.

이때, True 라면 우리가 원하는 요소를 생성할 수 있도록 해야겠죠?

targetType 는 드래그를 위해서 클릭한 아이콘이 가진 targetType 값을 가져옵니다.

저는 이 값을 “A” 또는 “B” 로 설정해두었습니다.

이 값에 따라서, 길이 높이 색깔이 조정됩니다.

그리고 여기서 핵심은 dragField.appendChild() 메소드를 사용해서.. dragField 라는 div 요소 내에 생성된 div 요소를 밀어서 넣어준다는 겁니다. 

마지막에는 드래그를 했던 요소를 다시 제자리로 돌려주어, 사용자에게 드래그가 성공한 것처럼 보여주면 되겠죠.

그럼 전체 코드입니다. 

				
					import React, { useState } from "react";
import {
  MdCabin,
  MdAbc,
  MdBackpack,
  MdSpaceBar,
  MdKayaking,
  MdBarChart,
} from "react-icons/md";
import "./Style.css";

//아이콘 드래그 시작시 위치 정보를 저장하는 함수
const dragStartHandler = (e) => {
  e.dataTransfer.setData("text/plain", null); // 필수: Firefox에서 드래그를 활성화하기 위함
  e.dataTransfer.setDragImage(e.target, 0, 0); // 드래그 이미지 설정

  // 드래그 시작 위치 저장
  e.target.setAttribute("data-start-x", e.clientX);
  e.target.setAttribute("data-start-y", e.clientY);

  // 요소의 원래 위치 저장
  e.target.setAttribute("data-original-x", e.target.offsetLeft);
  e.target.setAttribute("data-original-y", e.target.offsetTop);
};

//아이콘을 드래그에서 놓았을 때 작동하는 함수
const dragEndHandler = (e) => {
  const dragField = document.querySelector(".DragField");
  const box = dragField.getBoundingClientRect();
  const isInDragField =
    e.clientX >= box.left &&
    e.clientX <= box.right &&
    e.clientY >= box.top &&
    e.clientY <= box.bottom;

  if (isInDragField) {
    const targetType = e.target.getAttribute("targetType");
    const newElement = document.createElement("div");

    if (targetType === "A") {
      newElement.style.width = "100px";
      newElement.style.height = "100px";
      newElement.style.backgroundColor = "red";
    } else if (targetType === "B") {
      newElement.style.width = "150px";
      newElement.style.height = "150px";
      newElement.style.backgroundColor = "blue";
    }

    newElement.innerText = targetType;
    dragField.appendChild(newElement);
  }

  // 드래그된 요소를 원래 위치로 되돌림
  e.target.style.left = `${e.target.getAttribute("data-original-x")}px`;
  e.target.style.top = `${e.target.getAttribute("data-original-y")}px`;
};

//아이콘이 들어있는 사이드 메뉴
const SideBar = () => {
  return (
    <div className="SideMenu">
      <div className="item-line">
        <div
          className="item-icon"
          draggable="true"
          onDragStart={dragStartHandler}
          onDragEnd={dragEndHandler}
          targetType="A"
        >
          <MdCabin size={60} />
        </div>
        <div
          className="item-icon"
          draggable="true"
          onDragStart={dragStartHandler}
          onDragEnd={dragEndHandler}
          targetType="B"
        >
          <MdAbc size={60} />
        </div>
      </div>
      <div className="item-line">
        <MdBackpack size={60} />
        <MdSpaceBar size={60} />
      </div>
      <div className="item-line">
        <MdKayaking size={60} />
        <MdBarChart size={60} />
      </div>
    </div>
  );
};

//아이콘을 드래그 하여 특정한 필드를 생성하기 위한 공간
const MainBuilder = () => {
  return (
    <div className="MainBuilder">
      <div className="DragField">이곳에 아이콘을 드래그 할 것이다.</div>
    </div>
  );
};

const Page = () => {
  return (
    <div className="page">
      <SideBar />
      <MainBuilder />
    </div>
  );
};

export default Page;

				
			
				
					
.SideMenu{
    width: 200px;
    height: 100%;
    display: flex;
    flex-direction: column;
    background-color: aliceblue;
}

.item-line{
    display: flex;
margin-top: 15px;
margin-left: auto;
margin-right: auto;
}


.MainBuilder{
    width: 800px;
    min-height: 900px;
    flex-direction: column;
}

.DragField{
    display: flex;
    flex-direction: column;
    background-color: aquamarine;
    justify-content: center;
    margin: auto;
    width: 500px;
    height: 600px;
}

.item-icon{
    cursor: pointer;
}

.page{
    display: flex;
    margin: auto;
    height: 900px;
}
				
			

이제 어떤 식으로 코드가 작동이 되는지 한 번 보겠습니다.

어떻게 작동이 되는가?

실제 드래그를 위한 Box 내부가 아닌 곳에 드래그를 놓으면 작동이 되지 않습니다.

하지만, Box 내부에 아이콘을 놓으면 각기 다른 색깔의 div 가 생성되는 것을 볼 수 있습니다.

마치며...

이제 위 로직을 이용하여 자신만의 요소를 생성할 수 있을 겁니다.

각각의 아이콘에 지금은 targetType 이라는 값만을 넣었습니다만, 훨씬 여러 개의 값들을 지정하여 아이콘마다 다양한 고유값을 가질 수 있도록 하면 되겠죠?

이 코드를 기반으로 하여 조금 더 구체적으로 페이지를 꾸며야 겠습니다.