지도를 렌더링하는 웹 애플리케이션에서 마우스 이벤트를 활용해 자연스러운 드래그 이동과 줌 인/줌 아웃을 구현하는 것이 목표였습니다.
사용자 경험(UX)을 높이기 위해 다양한 마우스 동작을 정확하고 자연스럽게 처리하는 데 중점을 두었는데,
아래는 구현 과정에서의 시도와 문제 해결 방식을 정리한 내용입니다.
지도와 캔버스를 연동하는 과정에 대한 포스팅은 여기 에서 확인할 수 있습니다!!!!
처음에는 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) {
// 드래그에 따른 지도 이동 처리
}
};
줌 인/줌 아웃은 wheel
이벤트를 활용했습니다. 이벤트에서 deltaY
값을 읽어 줌을 조정했습니다.
const handleWheel = (e: React.WheelEvent) => {
const zoomChange = e.deltaY < 0 ? 1 : -1;
map.setZoom(map.getZoom() + zoomChange);
redrawCanvas();
};
드래그의 지연 문제를 해결하기 위해 시간 기반 조건을 추가했습니다. 클릭한 지 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));
}
};
줌 인/줌 아웃의 중심 좌표 문제를 해결하기 위해 휠 이벤트 발생 위치를 기준으로 중심을 재계산했습니다.
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();
};
캔버스 위에서 드래그 동작을 감지해 지도를 이동시키거나, 특정 동작을 구현하려고 할 때 다음과 같은 문제가 발생했습니다
드래깅을 감지하기 위해 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의 상태 관리 비동기성을 피하기 위해 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
를 활용해도 드래그가 의도대로 감지되지 않는 경우가 발생.드래그 문제를 반대로 접근하여, 기본적으로 캔버스의 pointer-events
를 'none'
으로 설정하고 클릭 시 'auto'
로 변경하는 방식을 시도했습니다.
canvasRef.current.style.pointerEvents = 'none';
const handleClick = () => {
canvasRef.current.style.pointerEvents = 'auto';
};
문제점:
네이버 지도 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;
}
문제점:
위의 시도들을 바탕으로 가장 안정적이고 효율적인 방식을 도출했습니다.
pointer-events
를 React 상태와 분리하여 직접 DOM 조작으로 관리.user-select: none
)로 대체.최종적으로 아래 코드로 마우스 이벤트를 구현했습니다. 여러 시도를 통해 자연스러운 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를 지원하지 않는가봐요....)