[지도-캔버스 연동] 마우스 이벤트 구현 과정

정혜인·2024년 11월 30일
0

🖱️ 프로젝트

지도를 렌더링하는 웹 애플리케이션에서 마우스 이벤트를 활용해 자연스러운 드래그 이동줌 인/줌 아웃을 구현하는 것이 목표였습니다.

사용자 경험(UX)을 높이기 위해 다양한 마우스 동작을 정확하고 자연스럽게 처리하는 데 중점을 두었는데,

아래는 구현 과정에서의 시도와 문제 해결 방식을 정리한 내용입니다.

지도와 캔버스를 연동하는 과정에 대한 포스팅은 여기 에서 확인할 수 있습니다!!!!


🎯 목표

  • 마우스 드래그: 지도와 캔버스를 함께 이동.
  • 마우스 휠: 줌 인/줌 아웃의 부드러운 전환.
  • 반응성: 사용자의 모든 동작을 빠르게 반영.

🛠️ 구현 과정

1. 기본 이벤트 처리

처음에는 mousedown, mousemove, mouseup 이벤트를 사용해 드래그를 구현했습니다. 드래그 시작 시 위치를 저장하고, mousemove에서 좌표를 업데이트하도록 했습니다.

const handleMouseDown = (e: React.MouseEvent) => {
  const rect = canvasRef.current?.getBoundingClientRect();
  setDragStartPos({
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
  });
};

const handleMouseMove = (e: React.MouseEvent) => {
  if (!dragStartTime) return;

  const timeElapsed = Date.now() - dragStartTime;
  if (timeElapsed > 300 && !isDragging) setIsDragging(true);

  if (isDragging) {
    // 드래그에 따른 지도 이동 처리
  }
};

문제

  • 드래그 시작 인식 지연: 마우스 클릭 후 약간의 지연이 발생.
  • 부정확한 좌표: 드래그 도중 마우스 위치와 실제 이동 좌표가 불일치.

2. 휠 이벤트 추가

줌 인/줌 아웃은 wheel 이벤트를 활용했습니다. 이벤트에서 deltaY 값을 읽어 줌을 조정했습니다.

const handleWheel = (e: React.WheelEvent) => {
  const zoomChange = e.deltaY < 0 ? 1 : -1;
  map.setZoom(map.getZoom() + zoomChange);
  redrawCanvas();
};

문제

  • 줌 전환 속도: 휠 스크롤이 너무 민감하게 반응해 줌이 과도하게 변화.
  • 중심 좌표 문제: 줌 인/줌 아웃 시 지도가 중심 좌표를 벗어나는 경우 발생.

3. 자연스러운 드래그 구현

드래그의 지연 문제를 해결하기 위해 시간 기반 조건을 추가했습니다. 클릭한 지 0.3초 이상 경과 후 isDragging 상태를 true로 설정해 드래그로 간주했습니다.

const handleMouseDown = (e: React.MouseEvent) => {
  setDragStartTime(Date.now());
  setDragStartPos({ x: e.clientX, y: e.clientY });
};

const handleMouseMove = (e: React.MouseEvent) => {
  if (!dragStartTime) return;

  const timeElapsed = Date.now() - dragStartTime;
  if (timeElapsed > 300 && !isDragging) setIsDragging(true);

  if (isDragging) {
    // 드래그 이동 처리
    map.panBy(new naver.maps.Point(dragDeltaX, dragDeltaY));
  }
};

개선점

  • 드래그가 의도치 않게 발생하는 문제 해결.
  • 지도 이동이 더 자연스럽게 작동.

4. 줌 중심 좌표 보정

줌 인/줌 아웃의 중심 좌표 문제를 해결하기 위해 휠 이벤트 발생 위치를 기준으로 중심을 재계산했습니다.

const handleWheel = (e: React.WheelEvent) => {
  const { offsetX, offsetY } = e.nativeEvent;

  // 휠 방향에 따른 줌 변경
  const zoomChange = e.deltaY < 0 ? 1 : -1;
  const zoom = map.getZoom() + zoomChange;

  // 이벤트 좌표를 중심으로 보정
  const latLng = projection.fromOffsetToCoord(new naver.maps.Point(offsetX, offsetY));
  map.setCenter(latLng);
  map.setZoom(zoom);

  redrawCanvas();
};

개선점

  • 줌 인/줌 아웃 시 화면 중심이 움직이는 문제 해결.
  • 사용자가 직관적으로 원하는 위치에 초점을 맞출 수 있도록 개선.

🚩 발생한 이슈

캔버스 위에서 드래그 동작을 감지해 지도를 이동시키거나, 특정 동작을 구현하려고 할 때 다음과 같은 문제가 발생했습니다

  1. 드래깅 동작 감지가 불완전
    드래그 시작 조건이나 타이밍이 맞지 않아 드래그가 시작되지 않거나, 의도하지 않은 동작을 유발함.
  2. 지도 위 UI 요소의 드래깅 간섭
    네이버 지도 API가 제공하는 UI 요소(저작권 표기, 로고 등)가 드래그 이벤트를 방해하며 예상치 못한 결과를 초래함.
  3. React 상태 관리와 DOM 조작 비동기성
    React 상태 업데이트와 DOM 변경 타이밍 간의 불일치로 인해 이벤트 처리가 원활하지 않았음.

1️⃣ 드래그 시작 조건과 흐름 제어

드래깅을 감지하기 위해 isDragging 상태를 도입했습니다.

const [isDragging, setIsDragging] = useState(false);

// 마우스 핸들러
const handleMouseDown = () => {
  setTimeout(() => setIsDragging(true), 300); // 0.3초 동안 클릭을 유지하면 드래깅 상태로 전환
};

const handleMouseUp = () => setIsDragging(false);

// 스타일 변경
useEffect(() => {
  canvasRef.current.style.pointerEvents = isDragging ? 'none' : 'auto';
}, [isDragging]);

문제점:

  • isDragging 상태가 변경되더라도 이미 시작된 마우스 드래그 동작은 즉시 영향을 받지 못함.
  • React 상태 관리 특성상 DOM 업데이트가 비동기적으로 이루어져 타이밍 문제 발생.

2️⃣ 드래그 타이밍과 DOM 조작 문제 해결

React의 상태 관리 비동기성을 피하기 위해 useRef를 사용하여 isDragging 상태를 관리하고, DOM 조작을 직접 처리하는 방식으로 변경했습니다.

const isDraggingRef = useRef(false);

const handleMouseDown = () => {
  isDraggingRef.current = true;
  canvasRef.current.style.pointerEvents = 'none';
};

const handleMouseUp = () => {
  isDraggingRef.current = false;
  canvasRef.current.style.pointerEvents = 'auto';
};

문제점:

  • isDraggingRef를 활용해도 드래그가 의도대로 감지되지 않는 경우가 발생.
  • 상태를 ref로 관리하면서 React 상태 업데이트와의 충돌 발생.

3️⃣ Pointer Events 기본값 변경

드래그 문제를 반대로 접근하여, 기본적으로 캔버스의 pointer-events'none'으로 설정하고 클릭 시 'auto'로 변경하는 방식을 시도했습니다.

canvasRef.current.style.pointerEvents = 'none';

const handleClick = () => {
  canvasRef.current.style.pointerEvents = 'auto';
};

문제점:

  • 클릭 동작도 드래그로 간주되어 지도가 이동하지 않음.
  • 사용자 경험이 떨어지고 클릭 동작과 드래깅 동작의 경계가 모호해짐.

4️⃣ 네이버 지도 UI 요소 간섭 제거

네이버 지도 API의 기본 UI 요소가 드래그 이벤트를 방해하는 문제를 발견했습니다.

이 요소들은 div 태그 내부에 있는 span, a, img로 구성되어 있으며, 클래스명이나 ID가 없어서 선택하기 어려웠습니다.

CSS를 통해 강제로 드래깅을 막았습니다.

span, a, img {
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}

문제점:

  • 드래깅 방해 요소 일부가 여전히 간섭.
  • UI 요소를 완전히 제거할 수 없으며, 공식 문서에 따르면 로고 제거는 불가능함.

🔑 최종 해결 방법

위의 시도들을 바탕으로 가장 안정적이고 효율적인 방식을 도출했습니다.

  1. 드래깅 동작 최적화
    • 드래그 시작과 종료 조건을 명확히 정의.
    • pointer-events를 React 상태와 분리하여 직접 DOM 조작으로 관리.
  2. UI 요소 드래그 간섭 제거
    • API 제공 UI 요소의 드래깅 방해를 최소화하기 위해 CSS 기반 접근을 유지.
    • 로고 제거 대신 사용자 선택 방지(user-select: none)로 대체.
  3. React와 DOM 조작의 역할 분리
    • DOM 조작은 직접 접근하며 React는 상태 관리를 위한 용도로만 사용.

5. 최종 선택된 코드와 이유

최종적으로 아래 코드로 마우스 이벤트를 구현했습니다. 여러 시도를 통해 자연스러운 UX를 제공하는 데 성공했습니다.

const handleMouseDown = (e: React.MouseEvent) => {
  setDragStartTime(Date.now());
  const rect = canvasRef.current!.getBoundingClientRect();
  setDragStartPos({
    x: e.clientX - rect.left,
    y: e.clientY - rect.top,
  });
};

const handleMouseMove = (e: React.MouseEvent) => {
  if (!dragStartTime) return;

  const timeElapsed = Date.now() - dragStartTime;
  if (timeElapsed > 300 && !isDragging) setIsDragging(true);

  if (isDragging) {
    const rect = canvasRef.current!.getBoundingClientRect();
    const dragEndPos = { x: e.clientX - rect.left, y: e.clientY - rect.top };

    // 지도 이동 처리
    const deltaX = dragStartPos.x - dragEndPos.x;
    const deltaY = dragStartPos.y - dragEndPos.y;
    map.panBy(new naver.maps.Point(deltaX, deltaY));
  }
};

const handleWheel = (e: React.WheelEvent) => {
  const zoomChange = e.deltaY < 0 ? 1 : -1;
  map.setZoom(map.getZoom() + zoomChange);
  redrawCanvas();
};

아래는 줌(마우스 휠)과 드래그를 구현했을 때의 모습입니다!
(gif 캡처 과정에서 조금.... 느리게 캡처된 점 양해 부탁드립니다.....ㅠㅠ)
(만약 이미지가 멈춰있다면, 이미지에서 오른쪽 마우스 클릭 후 새 탭에서 이미지 열기로 들어가주세요... 벨로그가 gif를 지원하지 않는가봐요....)

0개의 댓글