올리브영 테크블로그 포스팅 마우스 드래그로 범위 지정과 리사이징 및 이동 구현하기
Frontend

마우스 드래그로 범위 지정과 리사이징 및 이동 구현하기

기획전 마크업 자동화를 위한 드래그 범위 지정 기능 구현

2023.10.20

1
올리브영 기획전

올리브영 기획전에서는 카테고리별 테마와 함께 위클리 스페셜, 지금 진행중인 행사 등을 통해 여러가지 상품과 혜택을 제공하고 있습니다.

현재 올리브영 온라인몰의 기획전 컨텐츠는 마크업 자동 완성이 가능한 자체적으로 개발한 기획전 제작툴을 사용하여 제작되고 있습니다.

이러한 툴을 개발하게 된 배경과 툴에 들어간 드래그 범위 지정, 리사이징 및 이동 기능에 대해 소개를 드리겠습니다.

기존 기획전 작업 프로세스의 문제점

올리브영에서 고객들에게 여러가지 기획전을 제공하기 위한 실제 작업은 매월 약 100건 이상이 필요하고, 이렇게 발생하는 모든 기획전에 대한 마크업 작업은 건별로 직접 하드 코딩 하는 방식으로 작업이 이루어 졌습니다.

작업자체는 통이미지 위에 링크 영역 등을 잡는 어렵지 않은 작업이지만, 단순 반복적 업무로 인해 작업 효율이 매우 낮았고 불필요한 리소스가 소비되는 상황도 발생하게 되었습니다.

이렇게 비효율적인 작업 프로세스를 개선하기 위해 기획전 마크업 작업 자동화에 대한 필요성을 느끼게 되었고, 이를 구현하기 위해 필요한 기능에 대해 정의하게 되었습니다.

· 마우스 드래그를 통해 액션 영역을 지정하고 리사이징, 이동을 가능하게 한다.
· 지정된 영역 내의 드롭박스 선택 및 텍스트 입력 등을 통해 액션 유형 지정을 가능하게 한다.
· 액션 유형에 따라 마크업을 미리 지정해 놓고 지정된 영역들의 위치값을 전달하여 마크업이 자동으로 완성되게 한다.
· BO 에디터에 생성된 코드를 삽입하고 같은 방식으로 수정 가능하게 한다.
· BO 외부에서 업로드하던 기존 이미지 업로드 방식을 개선하여 액션 영역에서 가능하게 한다.

마우스 이벤트 객체의 위치 프로퍼티

4
마우스 이벤트 객체

위에서 정의한 기능 구현을 위해 우선 마우스 이벤트 객체에 대해 알아 보겠습니다.

마우스 이벤트 객체에는 마우스 위치에 대한 여러가지 프로퍼티들이 있습니다.

브라우저에서 마우스를 클릭하거나 드래그 또는 마우스 움직임에 따라 특정 요소를 반응 시키고자 할때 이러한 위치값이 필요하게 됩니다.

이러한 마우스 이벤트의 위치값은 어떠한 영역을 기준으로한 위치값인지 그 기준이 각자 다릅니다.


· screenX, screenY: 디바이스 전체 화면 영역을 기준으로 한 위치값.

· clientX, clientY: 브라우저 창의 뷰 영역을 기준으로 한 위치값.

· pageX, pageY: 웹 문서 전체 영역을 기준으로 한 위치값.

· offsetX, offsetY: 이벤트가 발생한 엘리먼트를 기준으로 한 위치값.


아래에서는 pageX, pageY 값을 예시로 실제 구현한 기획전 제작툴의 마우스 드래그를 통한 범위 지정, 리사이징, 이동 기능을 예제 코드를 통해 설명 드리겠습니다.

마우스 드래그로 영역 범위 지정 및 이동, 리사이즈 구현하기

박스 스타일 정의

.box {
  position: absolute;
  top: 0;
  left: 0;
  width: 1px;
  height: 1px;
  transform-origin: left top;
  background-color: rgb(0 0 0 / 60%);
}

드래그를 통해 유동적으로 변경될 박스의 크기와 위치는 자바스크립트를 통해 이벤트 발생시 Repaint, Reflow가 발생하지 않는 transform 속성의 translate와 scale로 지정됩니다.

X, Y값 기준으로 해당값만큼 Element를 이동시키는 translate를 사용하기 위해 position은 absolute, top과 left 값은 0으로 지정해줍니다.

X, Y값 기준으로 해당값의 n배만큼 축소나 확대하는 scale을 사용하기 위해 width와 height는 1px로 잡아줍니다.

드래그 이벤트 시작

class MouseDrag {
  constructor() {
    this.onDragStart();
    this.onMouseLeave();
  }

  onMouseLeave() {
    document.body.onmouseleave = () => {
      document.body.onmousemove = null;
    };
  }

  onDragStart() {
    document.body.onmousedown = (e) => {
      e.preventDefault();

      const dragStartLeft = e.pageX;
      const dragStartTop = e.pageY;
      const box = document.createElement("div");

      box.className = "box";
      document.body.appendChild(box);

      this.onDrag(box, dragStartLeft, dragStartTop);
      this.onDragEnd();
    };
  }

  // ...
}

const mouseDrag = new MouseDrag();

드래그를 위해 마우스 버튼을 누르면 이벤트가 시작됩니다.

박스를 생성해 클래스명을 부여하고 body 태그에 삽입 후 박스와 마우스 클릭이 시작된 X, Y 위치값을 onDrag 메소드로 전달해 줍니다.

마우스가 화면 밖으로 나가게 되면 마우스 이동시의 이벤트를 초기화 시킵니다.

드래그를 통한 박스 생성 구현

onDrag(box, dragStartLeft, dragStartTop) {
  document.body.onmousemove = (e) => {
    e.preventDefault();

    const x = e.pageX;
    const y = e.pageY;
    const left = x < dragStartLeft ? x : dragStartLeft;
    const top = y < dragStartTop ? y : dragStartTop;
    const width = Math.abs(dragStartLeft - x);
    const height = Math.abs(dragStartTop - y);

    box.style.transform = `translate(${left}px, ${top}px) scale(${width}, ${height})`;
  };
}

onDragEnd() {
  document.body.onmouseup = () => {
    document.body.onmousemove = null;
  };
}

마우스 드래그에 따라 박스의 크기와 위치를 계산해줍니다.

마우스 클릭이 시작된 위치값보다 현재 마우스 위치값이 작다면 현재의 위치값을 left, top 값으로 지정하고 그렇지 않다면 마우스 클릭이 시작된 위치값을 지정합니다.

마우스 클릭이 시작된 위치값에서 현재 마우스 위치값을 뺀 절대값으로 width와 height 값을 구해줍니다.

넘겨받은 box의 스타일 속성에서 transform에 translate와 scale에 값을 삽입하여 박스의 크기와 위치를 지정합니다.

onDragEnd에서는 마우스 이동시의 이벤트를 초기화 시켜 줍니다.

드래그를 통한 박스 생성 구현

드래그를 통한 박스 이동과 Resize 구현

class MouseDrag {
  constructor() {
    this.onDragStart();
  }

  onDragStart() {
    document.body.onmousedown = (e) => {
      e.preventDefault();

      const dragStartLeft = e.pageX;
      const dragStartTop = e.pageY;

      if (e.target.classList.contains("box")) {
        this.onDragMoveResize(e.target, dragStartLeft, dragStartTop);
      } else {
        const box = document.createElement("div");
        box.className = "box";
        document.body.appendChild(box);
        this.onDrag(box, dragStartLeft, dragStartTop);
      }

      this.onDragEnd();
    };
  }

  // ...
}

const mouseDrag = new MouseDrag();

드래그를 통한 박스 리사이징과 이동을 구현하기 위해 onDragStart에서 드래그 이벤트의 조건을 구분해 줍니다.

이미 만들어진 박스 위에서의 드래그는 리사이즈 또는 이동을 실행하고 그렇지 않다면 기존의 박스를 생성하는 드래그를 실행합니다.

onDragMoveResize(box, dragStartLeft, dragStartTop) {
  const padding = 20;

  // WebKitCSSMatrix를 사용하여 기존 박스 크기와 위치값 확인
  const style = window.getComputedStyle(box);
  const matrix = new WebKitCSSMatrix(style.transform);
  const boxPosition = {
    left: matrix.e,
    top: matrix.f,
    width: matrix.a,
    height: matrix.d,
  };

  const { left, top, width, height } = boxPosition;

  // 마우스 클릭이 시작된 위치값이 리사이즈 위치인지 확인
  const resizeBoxLeft = left <= dragStartLeft && dragStartLeft <= left + padding;
  const resizeBoxRight = left + width >= dragStartLeft && dragStartLeft >= left + width - padding;
  const resizeBoxTop = top <= dragStartTop && dragStartTop <= top + padding;
  const resizeBoxBottom = top + height >= dragStartTop && dragStartTop >= top + height - padding;

  document.body.onmousemove = (e) => {
    const x = e.pageX;
    const y = e.pageY;
    const isResize = (resizeBoxLeft && (resizeBoxTop || resizeBoxBottom)) || (resizeBoxRight && (resizeBoxTop || resizeBoxBottom));
    let { left, top, width, height } = boxPosition;

    if (isResize) { // 박스 리사이징을 위한 크기와 위치값 계산
      if (resizeRight) {
        width = x + boxPosition.width - dragStartLeft;
      }
      if (resizeBottom) {
        height = y + boxPosition.height - dragStartTop;
      }
      if (resizeTop) {
        top = y + boxPosition.top - dragStartTop;
        height = boxPosition.height + dragStartTop - y;
      }
      if (resizeLeft) {
        left = x + boxPosition.left - dragStartLeft;
        width = boxPosition.width + dragStartLeft - x;
      }
    } else { // 박스 이동을 위한 위치값 계산
        left += x - dragStartLeft;
        top += y - dragStartTop;
    }

    box.style.transform = `translate(${left}px, ${top}px) scale(${width}, ${height})`;
  };
}

리사이징과 이동을 구현하기 위해 기존 박스의 크기와 위치값을 구해야 합니다. 박스 생성에서 transform 속성에 삽입했던 translate와 scale 값을 구해야 하는데요. 위에서 translate와 scale값은 transform 속성에 String으로 삽입했습니다.

물론 String값에서 크기와 위치값을 발라내서 구할수도 있지만 코드에서와 같이 WebKitCSSMatrix를 사용한다면 transform 속성에 String으로 삽입했던 translate와 scale 값을 쉽게 구할수 있습니다.

이를 활용하여 마우스 클릭이 시작된 위치값이 드래그시 리사이즈를 시켜줄 위치인지 확인을 해줍니다. 코드에서는 박스의 네 귀퉁이에서 안쪽으로 20px 만큼을 Resizable area로 지정하였습니다.

박스 리사이징시 오른쪽이나 아래쪽의 드래그는 박스의 크기를 새로 구해주고, 왼쪽이나 윗쪽의 드래그는 크기와 함께 위치값도 함께 구해줍니다.

박스 이동을 위한 위치값 또한 마찬가지로 위의 코드와 같이 계산하여 구합니다.

드래그를 통한 박스 이동 구현
드래그를 통한 박스 Resize 구현

이렇게 구현하고자 했던 마우스 드래그와 관련된 기능 구현은 모두 완료 하였습니다!

실적용

기획전 제작툴 사용 화면

실제 개발된 기획전 제작툴에서는 위의 예제 코드와 같은 방법으로 마우스 드래그를 통해 영역을 지정하고, 지정한 영역을 리사이징하고 이동할수 있는 기능이 적용되었습니다.

이렇게 지정된 영역 내에서 작업자는 링크 영역을 설정하거나 이미지나 동영상을 삽입하는 등의 상세 액션 유형을 설정할수 있고, 디자인 시안에서의 모든 영역을 이러한 방법으로 지정만 해준다면 준비는 끝입니다.

최종 확인 버튼을 눌러 지정된 영역들에 해당하는 마크업 및 영역들의 크기와 위치값 스타일을 생성하고, 사용자가 입력한 값을 전달하여 액션 유형에 따라 미리 지정 되어있는 마크업을 조합하면 기획전 생성은 끝이 나게 됩니다.

마치며

기획전 제작툴이 개발되고 테스트 기간을 거쳐 작업자들이 기획전 작업을 위해 실제로 툴을 사용을 하게 되었습니다.

기존 작업자의 역할이었던 마크업 작업을 기획전 제작툴을 사용하여 대응이 가능하여 많은 리소스를 절감할수 있게 되었습니다.

이로 인해 기획전 제작을 위한 작업 프로세스가 간소화 되었고 작업 속도 및 효율을 증대시킬수 있었습니다.

이렇게 올리브영에서는 비효율적인 프로세스나 환경 개선을 위해 항상 고민하고 노력하고 있습니다.

앞으로의 행보도 지켜봐 주세요! 감사합니다!

OliveYoungFrontEnd
올리브영 테크 블로그 작성 마우스 드래그로 범위 지정과 리사이징 및 이동 구현하기
🎵
JK |
Retro dreamer