React Kakao Map SDK로 실시간 현위치 표시, 현위치로 중심좌표 이동하기

Minsu Han·2024년 4월 19일
5

들어가며

현재 개발중인 프로젝트에서, 지도 위에 내 실시간 위치를 표시하거나, 버튼을 누르면 현위치로 지도의 중심좌표를 이동하는 기능을 구현해야했다.

이전에 개인프로젝트 trip-mate는 쌩 자바스크립트로 제공되는 카카오맵 API를 사용했는데, 공식문서가 잘 되어있고 예시도 많은 건 사실이지만, React 프로젝트에 적용하기에는 코드도 길어지고 지도를 반복 생성하지 않도록 useEffect로 처리해줘야 하는 등 다소 불편한 점들이 있었다.

그래서 이번 프로젝트에서는 실력자분이 개발하신 React Kakao Map SDK를 사용해서 카카오맵 API를 컴포넌트 형태로 사용하기로 결정했다. 샘플코드를 보니 확실히 코드가 간결해지고 마커 추가를 하기에도 편리해 보였다.

실시간 현위치 표시하기

지도 위에 실시간으로 현위치를 표시하려면 다음 세 가지를 해야 한다.

  1. 화면에 지도 생성하기
  2. 기기 위치(위경도 값) 변화를 감지
  3. 지도에서 해당 위치에 마커 그리기

현재 위경도 좌표(position)과 지도의 중심좌표(center)를 각각 useState로 관리하기로 했다.

// 지도의 중심좌표
const [center, setCenter] = useState<LatLng>({
  lat: 33.450701,
  lng: 126.570667,
});

// 현재 위치
const [position, setPosition] = useState<LatLng>({
  lat: 33.450701,
  lng: 126.570667,
});

처음 컴포넌트가 렌더링되면 navigator.geolocation.getCurrentPosition() 함수로 현재 위경도 값을 얻어서, 지도의 중심좌표로 설정한 다음, 기기 이동이 감지될 때마다 position을 갱신하게 했다.

// 지도가 처음 렌더링되면 중심좌표를 현위치로 설정하고 위치 변화 감지
useEffect(() => {
  navigator.geolocation.getCurrentPosition((pos) => {
    setCenter({ lat: pos.coords.latitude, lng: pos.coords.longitude });
  });

  navigator.geolocation.watchPosition((pos) => {
    setPosition({ lat: pos.coords.latitude, lng: pos.coords.longitude });
  });
}, []);

이제 화면에 지도를 그리면 되는데, 중심좌표를 center로 하고, 현위치 좌표인 position 위치에 마커를 그리면 된다.

import { useEffect, useMemo, useState } from "react";
import { Map as KakaoMap, MapMarker } from "react-kakao-maps-sdk";

return (
    <div className="relative w-full h-full">
      <KakaoMap // 지도를 표시할 Container
        className="w-full h-full"
        id="map"
        center={center}
        level={4} // 지도의 확대 레벨
      >
        {/* 현위치 마커 */}
        <MapMarker
          image={{
            src: require("../../assets/markers/position.svg").default,
            size: { width: 30, height: 30 },
          }}
          position={position}
        />
      </KakaoMap>
    </div>
  );

이렇게 하면 지도 위에 내 위치가 마커로 표시되고, 위치가 바뀌면 마커가 따라서 이동하는 걸 볼 수 있다.

버튼을 누르면 현 위치로 중심좌표 이동하기

지도를 이리저리 움직이다 보면, 지도의 중심좌표도 계속 바뀐다.
그러다가 다시 현위치를 보고 싶으면 지도의 중심좌표를 현위치로 옮겨야 한다.

버튼을 하나 추가해서 그걸 누르면 setCenter(position)를 실행시켜서 중심좌표를 현위치로 설정하도록 해 보자. 지도 컴포넌트에서 center 프로퍼티 값으로 center 상태를 구독하니까, 아마 지도가 다시 현위치를 중심으로 표시될 것이다.

// 지도의 중심을 유저의 현재 위치로 변경
const setCenterToMyPosition = () => {
  setCenter(position);
};

return (
    <div className="relative w-full h-full">
      <KakaoMap // 지도를 표시할 Container
        className="w-full h-full"
        id="map"
        center={center}
        level={4} // 지도의 확대 레벨
      >
        {/* 현위치 마커 */}
        <MapMarker
          image={{
            src: require("../../assets/markers/position.svg").default,
            size: { width: 30, height: 30 },
          }}
          position={position}
        />
      </KakaoMap>
      <div className="flex flex-col gap-[10px] absolute z-[1] top-0 right-0 p-[10px]">
        {/* 중심좌표 재설정 버튼 */}
        <button
          className="flex justify-center items-center cursor-pointer rounded-full w-[45px] h-[45px] bg-white shadow-[0_0_8px_#00000025]"
          onClick={setCenterToMyPosition}
        >
          <IconMyLocation width={25} height={25} />
        </button>
      </div>
    </div>

그러나...

실행시켜보면 지도를 이동시키고 나서 버튼을 처음 눌렀을 땐 정상적으로 현위치로 중심이 이동하는데, 두 번째부터는 버튼을 눌러도 제대로 작동하지 않았다.

이 문제는 SDK 문서에서도 샘플코드를 실행해보면 동일한 현상이 발생했는데, 자바스크립트 API에서는 정상적으로 동작하는 걸 봐서는, 컴포넌트 안에서 관리중인 center 상태를, 지도 이동이 감지될 때에도 갱신해줘야 할 것 같았다.

해결 방법

카카오맵에서는 지도 이동 이벤트를 등록할 수 있기 때문에, 지도 컴포넌트의 onCenterChanged prop에 중심좌표 변경 이벤트 콜백함수를 등록해서, 지도가 이동하여 중심좌표가 변경되면 center 상태를 해당 좌표로 갱신하도록 했다.

다만 지도를 옮길 때마다 중심좌표 이동 감지가 스크롤 이벤트처럼 매우 빈번히 일어나기 때문에, 중심좌표 이동이 감지되고 500ms동안 추가적인 감지가 없을 때에만 center 상태를 갱신하기 위해 lodash에서 제공되는 debounce 함수를 사용했다. 그리고 지도 컴포넌트가 리렌더링 될 때마다 이 함수도 다시 생성되면서 타이머가 초기화될 수 있기 때문에, useMemo로 감싸서 타이머가 초기화되지 않게 했다.

// 지도 중심좌표 이동 감지 시 이동된 중심좌표로 설정
const updateCenterWhenMapMoved = useMemo(
  () =>
  debounce((map: kakao.maps.Map) => {
    console.log(map.getCenter());
    setCenter({
      lat: map.getCenter().getLat(),
      lng: map.getCenter().getLng(),
    });
  }, 500),
  []
);

return (
    <div className="relative w-full h-full">
      <KakaoMap // 지도를 표시할 Container
        className="w-full h-full"
        id="map"
        center={center}
        level={4} // 지도의 확대 레벨
        onCenterChanged={updateCenterWhenMapMoved}
      >
        {/* 현위치 마커 */}
        <MapMarker
          image={{
            src: require("../../assets/markers/position.svg").default,
            size: { width: 30, height: 30 },
          }}
          position={position}
        />
      </KakaoMap>
      <div className="flex flex-col gap-[10px] absolute z-[1] top-0 right-0 p-[10px]">
        <button
          className="flex justify-center items-center cursor-pointer rounded-full w-[45px] h-[45px] bg-white shadow-[0_0_8px_#00000025]"
          onClick={setCenterToMyPosition}
        >
          <IconMyLocation width={25} height={25} />
        </button>
      </div>
    </div>
  );

이렇게 수정하고 나서 실행해보면

제대로 작동한다!

전체 코드

import { useEffect, useMemo, useState } from "react";
import { Map as KakaoMap, MapMarker } from "react-kakao-maps-sdk";
import { LatLng } from "../../types/map";
import { debounce } from "lodash";
import { ReactComponent as IconRefresh } from "../../assets/icons/refresh.svg";
import { ReactComponent as IconMyLocation } from "../../assets/icons/my-location.svg";

export default function Map() {
  // 지도의 중심좌표
  const [center, setCenter] = useState<LatLng>({
    lat: 33.450701,
    lng: 126.570667,
  });

  // 현재 위치
  const [position, setPosition] = useState<LatLng>({
    lat: 33.450701,
    lng: 126.570667,
  });

  // 지도의 중심을 유저의 현재 위치로 변경
  const setCenterToMyPosition = () => {
    setCenter(position);
  };

  // 지도 중심좌표 이동 감지 시 이동된 중심좌표로 설정
  const updateCenterWhenMapMoved = useMemo(
    () =>
      debounce((map: kakao.maps.Map) => {
        console.log(map.getCenter());
        setCenter({
          lat: map.getCenter().getLat(),
          lng: map.getCenter().getLng(),
        });
      }, 500),
    []
  );

  // 지도가 처음 렌더링되면 중심좌표를 현위치로 설정하고 위치 변화 감지
  useEffect(() => {
    navigator.geolocation.getCurrentPosition((pos) => {
      setCenter({ lat: pos.coords.latitude, lng: pos.coords.longitude });
    });

    navigator.geolocation.watchPosition((pos) => {
      setPosition({ lat: pos.coords.latitude, lng: pos.coords.longitude });
    });
  }, []);

  return (
    <div className="relative w-full h-full">
      <KakaoMap // 지도를 표시할 Container
        className="w-full h-full"
        id="map"
        center={center}
        level={4} // 지도의 확대 레벨
        onCenterChanged={updateCenterWhenMapMoved}
      >
        {/* 현위치 마커 */}
        <MapMarker
          image={{
            src: require("../../assets/markers/position.svg").default,
            size: { width: 30, height: 30 },
          }}
          position={position}
        />
      </KakaoMap>
      <div className="flex flex-col gap-[10px] absolute z-[1] top-0 right-0 p-[10px]">
        <button className="flex justify-center items-center cursor-pointer rounded-full w-[45px] h-[45px] bg-white shadow-[0_0_8px_#00000025]">
          <IconRefresh width={20} height={20} />
        </button>
        <button
          className="flex justify-center items-center cursor-pointer rounded-full w-[45px] h-[45px] bg-white shadow-[0_0_8px_#00000025]"
          onClick={setCenterToMyPosition}
        >
          <IconMyLocation width={25} height={25} />
        </button>
      </div>
    </div>
  );
}
profile
기록하기

0개의 댓글

관련 채용 정보