지도 상호작용을 구현하면서, 처음에는 마우스 이벤트를 활용해 지도 이동과 확대/축소를 구현했습니다.
하지만 모바일 환경을 지원하기 위해 터치 이벤트를 추가해야 했습니다. 터치 이벤트는 마우스 이벤트와 기본적으로 비슷하지만, 일부 동작과 처리 방식이 달랐습니다. 예상했던 것보다 구현해야 할 로직이 더 많았고, 여러 가지 문제를 해결하며 많은 고민을 하게 됐습니다.
이번 포스팅에서는 마우스 이벤트로 구현된 지도 상호작용을 터치 이벤트로 확장한 과정을 작성해보려고 합니다.
제가 작업해야하는 주요 기술은 아래 3가지로 정리할 수 있었습니다.
이전 포스팅에서 작성한 것처럼, 마우스 이벤트로 지도 이동/줌은 구현해 둔 상황이었습니다.
모바일이 아닐 때에는 마우스 이벤트로, 모바일로 접속했을 때에는 터치 이벤트로 모두 동작할 수 있게 터치 이벤트를 구현해주어야 했습니다.
참고로 기존 마우스 이벤트는 아래와 같이 구현되었습니다!
마우스 이벤트는 1개의 포인터만 다루면 되고, 마우스의 이동, 클릭, 휠 이벤트가 명확히 구분되기 때문에 상대적으로 간단했습니다.
마우스 이벤트와 다르게 터치 이벤트를 구현하려면 마우스와 다른 접근이 필요했고, 마우스 이벤트와의 차이를 작성해보자면 아래와 같이 정리할 수 있었습니다.
e.touches.length
)에 따라 로직을 분기 처리해야 했습니다.그래서 저는 터치 이벤트를 추가하기 위해 아래와 같은 과정을 거쳤습니다.
터치 동작은 onTouchStart
, onTouchMove
, onTouchEnd
이벤트로 처리할 수 있습니다. 이를 통해 터치 시작, 움직임, 종료 시 필요한 상태를 업데이트하도록 기본 구조를 작성했습니다.
그래도 마우스보다 다행인 것은, mouseMove 이벤트에서는 클릭한 상태로 움직이는 것인지, 그냥 클릭만하고 끝나는 것인지 등을 분류해주는 작업이 어려웠는데,
터치 이벤트는 애초에 터치를 한 상태로 움직이는 이벤트가 touchMove로 존재했기 때문에, 터치된 상태로 움직이고 있는 건지를 분리하는 로직이 필요없다는 장점이 있었습니다.
(개인적으로 마우스 이동할 때 이 로직 짜는게 가장 어렵고 정답이 없었는데, 터치는 그러지 않아도 되어서 너무 좋았습니다…..ㅎㅎㅎ,,,)
const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 2) {
// 두 손가락 터치 시작
} else if (e.touches.length === 1) {
// 한 손가락 터치 시작
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length === 2) {
// 두 손가락으로 확대/축소
} else if (e.touches.length === 1) {
// 한 손가락으로 지도 이동
}
};
const handleTouchEnd = (e: React.TouchEvent) => {
// 터치 종료 시 상태 초기화
};
문제:
해결 방법:
onTouchStart
) 위치를 저장한 뒤, 터치가 움직이는 동안(onTouchMove
)의 좌표와 비교해 이동 거리를 계산했습니다.const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 1) {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
setDragStartPos({
x: e.touches[0].clientX - rect.left,
y: e.touches[0].clientY - rect.top,
});
setIsTouching(true);
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (isTouching && e.touches.length === 1) {
const rect = canvasRef.current?.getBoundingClientRect();
if (!rect) return;
const newX = e.touches[0].clientX - rect.left;
const newY = e.touches[0].clientY - rect.top;
const deltaX = dragStartPos.x - newX;
const deltaY = dragStartPos.y - newY;
map?.panBy(new naver.maps.Point(deltaX, deltaY));
setDragStartPos({ x: newX, y: newY });
}
};
문제:
해결 방법:
zoomOrigin
)으로 설정.const handleTouchStart = (e: React.TouchEvent) => {
if (e.touches.length === 2) {
const distance = Math.sqrt(
Math.pow(e.touches[0].clientX - e.touches[1].clientX, 2) +
Math.pow(e.touches[0].clientY - e.touches[1].clientY, 2),
);
setTouchStartDistance(distance);
const centerX = (e.touches[0].clientX + e.touches[1].clientX) / 2;
const centerY = (e.touches[0].clientY + e.touches[1].clientY) / 2;
const mapCenter = map?.getProjection().fromContainerPixelToLatLng(
new naver.maps.Point(centerX, centerY),
);
setTouchCenter(mapCenter);
}
};
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length === 2 && touchStartDistance) {
const newDistance = Math.sqrt(
Math.pow(e.touches[0].clientX - e.touches[1].clientX, 2) +
Math.pow(e.touches[0].clientY - e.touches[1].clientY, 2),
);
const zoomChange = (newDistance - touchStartDistance) / 30; // 비율 조정
const currentZoom = map?.getZoom() ?? 10;
map?.setOptions({ zoomOrigin: touchCenter });
map?.setZoom(currentZoom + zoomChange);
setTouchStartDistance(newDistance);
}
};
줌 비율 계산에서 거리 차이에 따라 줌 변화량을 직접 반영했더니 확대/축소가 너무 민감하게 반응했습니다. 이를 해결하기 위해 스케일링 비율을 조정하여 줌 변화량을 적절히 제한해주고 있습니다.
다만,,,, 그 임계값이 완벽하지 않기에 아직도 개선해야하는 부분 중 하나입니다……ㅠㅠㅠ
두 손가락의 중심을 기준으로 줌을 적용했지만, 중심 좌표가 잘못 계산되어 화면 밖으로 벗어나는 문제가 있었습니다.
이를 해결하기 위해 지도 좌표 변환(fromContainerPixelToLatLng
)을 통해 정확히 중심을 설정해주었습니다.
터치 이벤트는 마우스 이벤트와 기본 원리가 비슷하지만, 멀티 포인터를 처리해야 한다는 점과 터치 동작의 의도를 파악해야 한다는 점에서 추가적인 고민이 필요했습니다.
특히 두 손가락 줌의 중심을 정확히 계산하고, 동작의 자연스러움을 유지하는 것이 가장 큰 도전 과제였습니다.
마우스에서는 클릭한 상태로 이동하는 것을 추적하는게 쉽지 않았다면, 오히려 터치이벤트는 이동이 아니라 줌을 얼마나 할지에 대해 정하는 것이 쉽지 않았던 것 같습니다.
하지만 결론적으로 아래와 같이 구현했고, 터치 이벤트가 잘 동작하는 것도 확인할 수 있었습니다 ㅎㅎ
(만약 이미지가 멈춰있다면, 이미지에서 오른쪽 마우스 클릭 후 새 탭에서 이미지 열기로 들어가주세요... 벨로그가 gif를 지원하지 않는가봐요....)
(이 때 진짜 너무 뿌듯해서 날아가는 줄 알았어요;; 또 발생할 문제는 모른채…)