최근 프로젝트에서 사용자가 레이어 팝업을 자유롭게 드래그 앤 드롭하여 화면 내에서 이동시킬 수 있는 기능이 필요했다.
이동된 위치 정보를 서버에 저장 할 필요가 없고, 단순히 프론트엔드 측에서만 처리하면 되는 기능이였다.
구현 계획
- 이동하고자 하는 요소는 "position: absolute;" 속성이 필수적으로 지정되있어야 한다.
( JavaScript 로 요소에 스타일을 지정해도 무관 ) - 요소의 위치 변경은 성능 최적화, 부드러운 애니메이션을 위하여 'style' 속성의 'left'와 'top' 값을 조정하는 대신 'transform' 의 'translate' 값을 변경한다.
enableDraggable 함수 구현 ( 모바일 미지원 버젼)
function enableDraggable(selector) {
let isDragging = false;
let dragStartX, dragStartY;
let initTranslateX = 0,
initTranslateY = 0;
const draggableEle = document.querySelector(selector);
const onPointerDown = (e) => {
isDragging = true;
dragStartX = e.pageX;
dragStartY = e.pageY;
const transform = window.getComputedStyle(draggableEle).transform;
if (transform !== 'none') {
const style = window.getComputedStyle(draggableEle);
const matrix = new DOMMatrixReadOnly(style.transform);
initTranslateX = matrix.m41;
initTranslateY = matrix.m42;
} else {
initTranslateX = 0;
initTranslateY = 0;
}
};
const onPointerMove = (e) => {
if (!isDragging) return;
const moveX = e.pageX - dragStartX;
const moveY = e.pageY - dragStartY;
const translateX = initTranslateX + moveX;
const translateY = initTranslateY + moveY;
draggableEle.style.transform = `translate(${translateX}px, ${translateY}px)`;
};
const onPointerUp = () => {
isDragging = false;
};
draggableEle.addEventListener('mousedown', onPointerDown);
document.addEventListener('mousemove', onPointerMove);
document.addEventListener('mouseup', onPointerUp);
}
- 코드 설명
- onPointerdown 함수
isDragging = true;
dragStartX = e.pageX;
dragStartY = e.pageY;
- onPointer 함수는 마우스 버튼이 눌렸을 때 (mousedown 이벤트) 호출됩니다.
- isDragging 변수를 true로 설정하여 드래그가 시작되었음을 나타냅니다.
- e.pageX와 e.pageY는 이벤트가 발생한 시점에서 마우스의 문서 상의 좌표를 나타냅니다.
이 좌표들은 드래그 시작 위치(dragStartX, dragStartY)에 저장됩니다.
이렇게 저장하는 이유는 드래그 동작 중 마우스 이동 거리를 계산하기 위함입니다. - e.clientX와 e.clientY 대신 e.pageX, e.pageY를 사용하는 이유는 페이지가 스크롤되어도 정확한 전체 문서상의 위치를 제공하기 때문입니다.
스크롤 된 거리를 고려하지 않으면, 요소의 위치 계산이 잘못될 수 있습니다.
const transform = window.getComputedStyle(draggableEle).transform;
if (transform !== 'none') {
const style = window.getComputedStyle(draggableEle);
const matrix = new DOMMatrixReadOnly(style.transform);
initTranslateX = matrix.m41;
initTranslateY = matrix.m42;
} else {
initTranslateX = 0;
initTranslateY = 0;
}
- window.getComputedStyle(draggableEle).transform을 통해 해당 요소에 적용된 transform 스타일 값을 가져옵니다.
- transform 값이 'none'이 아니라면, 즉 요소에 이미 변환(transform)이 적용된 경우, 해당 변환을 나타내는 매트릭스에서 초기 translate 값을 추출합니다.
- DOMMatrixReadOnly 객체는 CSS 변환을 나타내는 매트릭스를 다루기 위한 인터페이스입니다.
이 객체를 사용하여 transform 속성에 적용된 이동, 회전, 스케일 등의 변환 요소에 접근할 수 있습니다. - matrix.m41과 matrix.m42는 각각 매트릭스에서 x축과 y축으로의 이동 값을 나타냅니다.
이 값들을 initTranslateX, initTranslateY에 저장하여 드래그 시작 시 요소의 초기 위치(이동 값)로 사용합니다.
DOMMatrixReadOnly 객체에 더 자세한 정보를 원하신다면 아래 링크를 참조해주세요.
https://developer.mozilla.org/en-US/docs/Web/API/DOMMatrixReadOnly
DOMMatrixReadOnly - Web APIs | MDN
The DOMMatrixReadOnly interface represents a read-only 4×4 matrix, suitable for 2D and 3D operations. The DOMMatrix interface — which is based upon DOMMatrixReadOnly—adds mutability, allowing you to alter the matrix after creating it.
developer.mozilla.org
- onPointMove 함수
if (!isDragging) return;
// 드래그한 거리를 계산.
const moveX = e.pageX - dragStartX;
const moveY = e.pageY - dragStartY;
const translateX = initTranslateX + moveX;
const translateY = initTranslateY + moveY;
draggableEle.style.transform = `translate(${translateX}px, ${translateY}px)`;
- onPointerMove 함수는 마우스를 움직일 때 호출됩니다.
만약 isDragging이 false라면 (즉, 드래그가 시작되지 않았다면) 함수를 종료합니다. - moveX와 moveY는 드래그 동작 중 마우스의 이동 거리를 계산합니다.
이는 현재 마우스 위치에서 드래그 시작위치를 뺀 값입니다. - 계산된 이동 거리(moveX, moveY)를 드래그 시작 시 요소의 초기 translate값(initTranslateX, initTranslateY)에 더합니다.
이렇게 해서 얻은 총 이동 거리를 translate 값으로 설정하여, 요소의 위치를 업데이트합니다. - 요소의 스타일을 transform: translate(x, y)로 설정함으로써, 요소를 새 위치로 이동시킵니다.
모바일에서도 지원하기
모바일 지원을 추가하기 위해서는 마우스 좌표 대신 터치 좌표를 사용하면 됩니다.
모바일 환경에서는 e.touches[0].pageX와 e.touches[0].pageY를 사용하여 좌표를 얻습니다.
이를 위해 아래와 같이 함수를 정의하여 모바일 환경에 비모바일 환경을 구분하고, 각 환경에 맞게 pageX, pageY 값을 조정합니다.
function getPointerPosition(e) {
let pageX, pageY;
// 이벤트가 발생했을 때 터치 포인트의 존재와 그 수를 확인하여 모바일 터치 이벤트가 발생했는지를 판별
if (e.touches && e.touches.length > 0) {
pageX = e.touches[0].pageX;
pageY = e.touches[0].pageY;
} else {
pageX = e.pageX;
pageY = e.pageY;
}
return { pageX, pageY };
}
최종 코드
function enableDraggable(selector) {
let isDragging = false;
let dragStartX, dragStartY;
let initTranslateX = 0,
initTranslateY = 0;
const draggableEle = document.querySelector(selector);
const onPointerDown = (e) => {
e.preventDefault();
isDragging = true;
const { pageX, pageY } = getPointerPosition(e);
dragStartX = pageX;
dragStartY = pageY;
const transform = window.getComputedStyle(draggableEle).transform;
if (transform !== 'none') {
const style = window.getComputedStyle(draggableEle);
const matrix = new DOMMatrixReadOnly(style.transform);
initTranslateX = matrix.m41;
initTranslateY = matrix.m42;
} else {
initTranslateX = 0;
initTranslateY = 0;
}
};
const onPointerMove = (e) => {
if (!isDragging) return;
const { pageX, pageY } = getPointerPosition(e);
const moveX = pageX - dragStartX;
const moveY = pageY - dragStartY;
const translateX = initTranslateX + moveX;
const translateY = initTranslateY + moveY;
draggableEle.style.transform = `translate(${translateX}px, ${translateY}px)`;
};
const onPointerUp = () => {
isDragging = false;
};
draggableEle.addEventListener('mousedown', onPointerDown);
document.addEventListener('mousemove', onPointerMove);
document.addEventListener('mouseup', onPointerUp);
draggableEle.addEventListener('touchstart', onPointerDown);
document.addEventListener('touchmove', onPointerMove);
document.addEventListener('touchend', onPointerUp);
function getPointerPosition(e) {
let pageX, pageY;
if (e.touches && e.touches.length > 0) {
pageX = e.touches[0].pageX;
pageY = e.touches[0].pageY;
} else {
pageX = e.pageX;
pageY = e.pageY;
}
return { pageX, pageY };
}
}
모바일 환경에서 요소를 드래그 할 때, 화면 스크롤을 방지하기 위해 'onPointerDown' 이벤트에 e.preventDefault(); 를 추가 하였습니다.
모바일 지원 여부와 상관없이, 드래그 대상이 텍스트나 이미지, <button>, <a> 태그 등일 경우에는, 텍스트 선택 또는 링크 클릭과 같은 의도치 않은 동작을 방지할 수 있습니다.
CodePen 데모
See the Pen Untitled by 허강현 (@btavqxss-the-vuer) on CodePen.
'JavaScript' 카테고리의 다른 글
[JS/JavaScript] JavaScript로 쿠키(Cookie) 설정,읽기,삭제 구현 (0) | 2024.08.03 |
---|---|
[JS/JavaScript] Video Controls 커스텀하기 - 2 (0) | 2024.08.02 |
[HTML/CSS] Video Controls 커스텀하기 - 1 (0) | 2024.08.01 |
[JS/JavaScript] 드래그하여 체크박스 선택/해제하기 (0) | 2024.03.13 |
[Js/JavaScript] 텍스트를 클립보드에 복사하는 2가지 방법 (2) | 2024.03.13 |