[리뷰] 드래그 이벤트 제어하기(마우스, 터치)

Goyoung2·2023년 1월 1일
0

안녕하세요~ HTML 엘리먼트를 마우스로 옮기는 방법을 적어볼게요~

드래그

드래그는 클릭시작, 이동, 클릭끝 3단계로 이뤄지는데요. 이를 이벤트 함수로 등록해서 드래그를 제어할 수 있어요.

단 주의할 점은 마우스와 터치 이벤트의 속성이 조금 다르다는 건데요. 이를 해결하기 위해 약간의 조건문이 추가되어야해요.

draggable

저는 draggable 이라는 이벤트 제어 함수를 만들었구요. 이를 컴포넌트 마운트 시에 실행하도록 했어요.
draggable 내부에는 이벤트 상태를 제어하기 위한 변수들과, start, move, end 함수가 있어서 각 이벤트가 발생할때 실행되도록 했어요.
이벤트 추가는 마우스와 터치를 따로 등록시켜줘야해요

// 엘리먼트에 이벤트 등록
eventTarget.onmousedown = start
window.onmousemove = move
eventTarget.onmouseup = end

eventTarget.ontouchstart = start
window.ontouchmove = move
eventTarget.ontouchend = end

start

start 함수를 살펴볼게요. start 함수는 e(마우스이벤트 또는 터치이벤트)를 파라미터로 받고 pressed 상태를 true로 변경해요. e에 clientY가 있으면 마우스 이벤트로 인식하고 touches가 있으면 터치 이벤트로 인식해서 prevPosX/Y에 마우스의 위치를 저장해요. 참고로 마우스의 위치는 브라우저 기준 왼쪽 위가 (0,0)이고 오른쪽 아래로 갈 수록 수치가 늘어나요. 그리고 터치는 여러개일 수 있기 때문에 배열형태에요.

// 드래그 시작
function start(e: MouseEvent | TouchEvent) {
	pressed = true
    if ('clientY' in e) {
    	prevPosY = e.clientY
    } else if ('touches' in e) {
    	prevPosY = e.touches[0].clientY
    }
}

move

move 함수를 살펴볼게요. move 함수는 e를 파라미터로 받고 pressed 상태가 아니라면 함수를 종료해요. pressed 상태이면 diffX,Y를 계산하고, 현재 마우스의 위치를 prevPosX/Y에 저장해요. 그런다음 움직이고 싶은 엘리먼트(movingTarget) 스타일의 left, top을 변경해서 현재 마우스의 위치로 엘리먼트가 이동되도록 만듭니다. 추가적으로 엘리먼트를 처음 위치로 돌리고 싶다면 처음 위치를 startLeft/Top 변수에 저장해두고 end 함수에서 활용하면 돼요.
참고로 window에 이벤트를 추가해줍니다.

// 드래그 중
function move(e: MouseEvent | TouchEvent) {
	if (!pressed) return
	let diffY = 0
	if ('clientY' in e) {
		diffY = prevPosY - e.clientY
		prevPosY = e.clientY
	} else if ('touches' in e) {
		diffY = prevPosY - e.touches[0].clientY
		prevPosY = e.touches[0].clientY
	}
	if (movingTarget) {
		// 현재 위치 옮기기
		movingTarget.style.top = movingTarget.offsetTop - diffY + 'px'
		// startTop 저장
        if (startTop === '') startTop = movingTarget.offsetTop - diffY + 'px'
	}
}

end

end 함수를 살펴볼게요. 마우스 드래그가 끝날때는 pressed를 false로 초기화 시켜줘요. 추가적으로 다양한 액션을 추가할 수 있어요. 초기 위치로 되돌리고 싶다면 아래 코드를 참고하세요~
참고로 movingTarget은 실제 움직이는 엘리먼트이고, eventTarget 드래그 이벤트를 등록하는 엘리먼트에요.(드래그 손잡이) wrapperTarget은 딤처리된 백그라운드인데 위 타겟들의 상위 엘리먼트에요. 전체 엘리먼트를 제거할때 사용하고 있어요.

// 드래그 끝
function end() {
	pressed = false
    // 초기 위치로 되돌리기
	const delay = 300
	if (movingTarget && wrapperTarget) {
		// 트랜지션 잠깐 추가
		movingTarget.style.transition = `top ${delay}ms ease-out`
		// 1/3 이상 내렸으면 닫기
		if (prevPosY >= startPosY + (rootHeight - startPosY) / 3) {
			movingTarget.style.top = '100vh'
			setTimeout(() => {
				wrapperTarget.remove()
				setOpen?.(false)
			}, delay)
		} else {
			// 그렇지 않으면 원상복귀
			movingTarget.style.top = startTop
		}
		// 트랜지션 제거
		setTimeout(() => {
			movingTarget.style.transition = ``
		}, delay)
	}
}

전체코드(리액트)

아래는 모달바텀(액션시트)의 전체코드에요. 참고하시면 될것 같아요

const [open, setOpen] = useState(true)

// 마우스 드래그 이벤트
  const draggable = useCallback(
    (
      // wrapper > draggable >= moving
      eventTarget: HTMLElement, // 이벤트 등록 엘리먼트
      movingTarget?: HTMLElement, // 실제로 움직이는 엘리먼트
      wrapperTarget?: HTMLElement, // 전체 wrapper
    ) => {
      let isPress = false
      let prevPosY = 0
      if (!movingTarget) movingTarget = eventTarget
      const rootHeight = document.querySelector('#root')?.clientHeight || 0
      const startPosY = movingTarget.offsetTop
      let startTop = ''

      // 엘리먼트에 이벤트 등록
      eventTarget.onmousedown = start
      window.onmousemove = move
      eventTarget.onmouseup = end

      eventTarget.ontouchstart = start
      window.ontouchmove = move
      eventTarget.ontouchend = end

      // 드래그 시작
      function start(e: MouseEvent | TouchEvent) {
        if ('clientY' in e) {
          prevPosY = e.clientY
        } else if ('touches' in e) {
          prevPosY = e.touches[0].clientY
        }
        isPress = true
      }

      // 드래그 중
      function move(e: MouseEvent | TouchEvent) {
        if (!isPress) return
        let posY = 0
        if ('clientY' in e) {
          posY = prevPosY - e.clientY
          prevPosY = e.clientY
        } else if ('touches' in e) {
          posY = prevPosY - e.touches[0].clientY
          prevPosY = e.touches[0].clientY
        }
        if (movingTarget) {
          // 현재 위치 옮기기
          movingTarget.style.top = movingTarget.offsetTop - posY + 'px'
          // startTop 저장
          if (startTop === '') startTop = movingTarget.offsetTop - posY + 'px'
        }
      }

      // 드래그 끝
      function end() {
        isPress = false
        const delay = 300
        if (movingTarget) {
          // 트랜지션 잠깐 추가
          movingTarget.style.transition = `top ${delay}ms ease-out`
          // 1/3 이상 내렸으면 닫기.
          if (prevPosY >= startPosY + (rootHeight - startPosY) / 3) {
            movingTarget.style.top = '100vh'
            setTimeout(() => {
              wrapperTarget?.remove()
              setOpen?.(false)
            }, delay)
          } else {
            // 그렇지 않으면 원상복귀
            movingTarget.style.top = startTop
          }
          // 트랜지션 제거
          setTimeout(() => {
            movingTarget.style.transition = ``
          }, delay)
        }
      }
    },
    [setOpen],
  )

참고

profile
프론트엔드 엔지니어로 일하고 있어요. 제품, 동료, 성장을 중요시해요. 겸손, 존중, 신뢰를 갖춘 동료가 되기 위해 노력해요. 😄

0개의 댓글