[React] 특정 위치에 드래그/드롭 기능 구현하기

허지예·2023년 3월 8일
0

React 기능 구현

목록 보기
1/3
post-custom-banner

React로 게임 형식의 웹 프로젝트를 개발하면서 드래그 드롭 기능을 담당하게 되어 제작하게 되었다.

흔히 UI 개발에서 쓰는 드래그 드롭 형식이 아니라 게임처럼 어떤 요소를 특정 위치에 드래그하면 성공하게끔 하는 기능이다.

여러 파트에서 가져다 쓰기 변하게끔 src/services 폴더에 모듈로 구현하여, 다른 개발자들이 해당 기능을 사용하려면 해당 모듈을 import하고 setDragEvent()를 사용해 간편하게 사용할 수 있게 개발하였다.

다음은 기능을 구현한 모듈의 파일 구조이다.

src
 ㄴ services
 	 ㄴ dragEvent
     	 ㄴ index.js : 실제 기능을 구현한 코드
         ㄴ example.jsx : dragEvent 사용 예시 컴포넌트
         ㄴ example.module.css : 예시 컴포넌트를 위한 style 파일

사용 방법

작성한 모듈의 setDragEvent() 함수를 사용하면 쉽게 설정할 수 있게 하였다.

import { setDragEvent } from "../services/dragEvent";
/**
 * @param {element} 드래그를 할 요소 (position: absolute)
 * @param {element} 드래그를 마칠 목표 위치에 있는 요소 (position: absolute)
 * @param {function()} 드래그를 성공했을 때 실행할 callback 함수
 * @param {boolean} true 설정 시 드래그 성공 인식하는 위치 표시함 (생략 가능 default false)
 */

// class component에서 componentDidMount() 함수 내에 작성
// (dragEvent/example.jsx 참고)
setDragEvent(object, target, callback, [debug]);

구현 방법

작성한 dragEvent의 작동 원리는 다음과 같다.

  1. object에서 mousedown을 하고 나서, mousemove를 하면 object가 마우스를 따라온다 (드래그)
  2. target 위가 아닌 다른 곳에서 mouseup을 할 경우, object는 원래 자리로 돌아온다. (드롭 실패)
  3. target 위에서 mouseup을 할 경우, 성공 콜백 함수가 실행된다. (드롭 성공)

이 기능의 전제는 object와 target 모두 style 속성에서 position 값이 absolute라는 것이다.
(고정된 화면에서 진행되는 게임 형식의 웹 프로젝트이기 때문에 그렇다)


드롭 성공 여부를 판별하기 위해서 target 위에서 mouseup event를 인식해야 하는데, object가 마우스를 따라오면서, target 보다 레이어가 위에 있도록 하고 싶었다.

그래서 다음과 같이 target 위에 mouseup 인식용 요소를 생성하게 했다.

object보다도 위에 있을 listener 요소(아래 이미지에서 초록색)를 생성하고, 이 요소를 투명하게 해서 맨 처음 예시와 같이 작동하게 했다.

const create_targetListener = (target) => {
  const targetStyles = window.getComputedStyle(target);

  const listener = document.createElement("div"); // target 위에 새로 요소를 생성한다.

  /* target 위에 listener가 자리하도록 적절하게 style을 세팅해준다. */
  listener.style.cssText = `
    position: absolute; 
    background: green;
    border-radius: 50%;
    opacity: 0.5;
    width: ${targetStyles.width};
    height: ${targetStyles.height};
    top: ${targetStyles.top};
    left: ${targetStyles.left};
    z-index: 1001;`;
  find_absoluteStandard(target).append(listener); // listener를 적절하게 DOM에 넣어준다.

  return listener;
};

전체 코드 (src/services/dragEvent/index.js)

const find_absoluteStandard = (element) => {
  let parent = element.parentNode;
  while (
    (parent.position === "static" || parent.position === "") &&
    parent != document.body
  ) {
    parent = parent.parentNode;
  }
  return parent;
};

const create_targetListener = (target, debug) => {
  const targetStyles = window.getComputedStyle(target);
  const opacity = debug ? 0.5 : 0;

  const listener = document.createElement("div");
  listener.style.cssText = `
    position: absolute; 
    background: green;
    border-radius: 50%;
    opacity: ${opacity};
    width: ${targetStyles.width};
    height: ${targetStyles.height};
    top: ${targetStyles.top};
    left: ${targetStyles.left};
    z-index: 1001;`;
  find_absoluteStandard(target).append(listener);

  return listener;
};

/**
 * @param {element} 드래그를 할 요소 (position: absolute)
 * @param {element} 드래그를 마칠 목표 위치에 있는 요소 (position: absolute)
 * @param {function()} 드래그를 성공했을 때 실행할 callback 함수
 * @param {boolean} true 설정 시 드래그 성공 인식하는 위치 표시 (생략 가능 default false)
 */
export const setDragEvent = (object, target, callback, debug) => {
  const initPosition = {
    x: object.style.top,
    y: object.style.left,
    z: object.style.zIndex,
  };
  const initObject = () => {
    object.style.top = initPosition.x;
    object.style.left = initPosition.y;
    object.style.zIndex = initPosition.z;
  };

  let is_dragging = false;

  const standard = find_absoluteStandard(object);
  standard.addEventListener("mousemove", (event) => {
    const standardClient = standard.getBoundingClientRect();
    console.log(object.clientWidth * scale);
    if (is_dragging) {
      object.style.left = `${
        event.clientX - standardClient.left - object.clientWidth / 2
      }px`;
      object.style.top = `${
        event.clientY - standardClient.top - object.clientHeight / 2
      }px`;
    }
  });

  const listener = create_targetListener(target, debug);
  listener.addEventListener("mouseup", () => {
    if (is_dragging) callback();

    is_dragging = false;
    initObject();
  });

  document.body.addEventListener("mouseup", () => {
    is_dragging = false;
    initObject();
  });
  object.onmousedown = () => {
    is_dragging = true;
    object.style.zIndex = "1000";
  };
};

고정 비율 화면 만들기 적용했을 시

고정 비율 화면 만들기 적용했을 시에 다음과 같이 코드 수정
  1. responsiveFrame/index.js 파일에 밑에 함수 추가 작성하기

    /**
     * @return {number} 원본 화면 크기와 현재 화면 크기 비율 (현재 화면 크기 / 원본 화면 크기)
     */
    export const getFrameScale = () => {
      return document.querySelector("#App").style.zoom;
    };
  2. dragEvent/index.js의 일부를 다음과 같이 수정한다.

     import { getFrameScale } from "../responsiveFrame"; // import 추가
     
     ... 
     
     // setDragEvent() 내부의 드래그 기능 부분
     const standard = find_absoluteStandard(object);
     standard.addEventListener("mousemove", (event) => {
       const standardClient = standard.getBoundingClientRect();
       const scale = getFrameScale(); // 이 코드 추가
       
       if (is_dragging) {
     	 // 마우스 드래그하고 있는 좌표 값을 scale로 나누어 조정한다.
         object.style.left = `${
           event.clientX / scale - standardClient.left - object.clientWidth / 2
         }px`;
         object.style.top = `${
           event.clientY / scale - standardClient.top - object.clientHeight / 2
         }px`;
       }
     });
profile
대학생에서 취준생으로 진화함
post-custom-banner

0개의 댓글