Naver Map 자유롭게 활용하기

SilverBeen·2022년 2월 3일
74

Next.Js 를 기반으로 제작했습니다.
네이버 지도를 구현하기 위하여, Naver cloud platform - Web Dynamic Map을 사용했습니다.

사전 준비 사항

Naver cloud platform에서 map을 사용하기 위한 client-id를 발급받습니다.
Typescript에서 naver-map을 자유롭게 사용 할 것이므로 다음과 같은 라이브러리를 설치해줍니다.

npm install @types/navermaps 

만들고자 하는 것

요구 사항

✅ 왼쪽에서는 상점 리스트가 있고 오른쪽은 지도가 보입니다.
✅ 지도에는 상점 위치와 맞는 마커들이 찍혀 있는 상태이고, 마커를 누르면 자연스럽게 지도가 움직이며 선택한 마커가 중심으로 오도록 합니다.
✅ 왼쪽에 있는 상점 리스트를 클릭할 시 지도가 움직이는 것과 마찬가지로 지도가 자연스럽게 이동합니다.
✅ 선택된 마커는 하이라이트가 들어간 마커로 변경해야 합니다.

현재 위치 추적하기

지도를 생성하기에 앞서, 우리는 사용자의 현재 위치를 추적하여 사용자 화면에 맞는 지도를 띄워 줄 것입니다.

자바스트립트에서 제공하고 있는 navigator.geolocation API를 사용하여 사용자의 위치를 추적할 것입니다.

const [myLocation, setMyLocation] = useState<{ latitude: number; longitude: number } | string>('');

//현재 위치를 추적합니다.
useEffect(() => {
  if (navigator.geolocation) {
    navigator.geolocation.getCurrentPosition(success, error);
  }

  // 위치추적에 성공했을때 위치 값을 넣어줍니다.
  function success(position: any) {
    setMyLocation({
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
    });
  }

  // 위치 추적에 실패 했을때 초기값을 넣어줍니다.
  function error() {
    setMyLocation({ latitude: 37.4979517, longitude: 127.0276188 });
  }
}, []);

나의 위치를 담아줄 myLocation의 useState를 만들어 공간을 확보해줍니다.

navigator.geolocation.getCurrentPosition(success, error) 이 성공했을때와, 실패했을 때의 결과 값을 함수로 만들어 주어 그의 맞는 결과 값을 넣어줍니다.

현재 위치 스토리보드

  1. 사용자가 브라우저에 들어오게 된 경우, 브라우저는 위치 추적을 허용/차단을 사용자에게 물어보게 됩니다.
  2. 사용자가 허용을 클릭했다면 success 함수가 실행이 되고, 차단을 클릭했다면 error로 넘어가게 됩니다.
  3. 차단 했을 경우의 장소의 초기 좌표 값을 넣어줍니다. (초기값은 강남역으로 설정했습니다)

배포를 했는데 위치 추적이 안돼요!

localhost에서는 잘 됐던 기능이 배포를 하니까 위치 추적이 안되는 경우가 발생 할 수 있습니다.
이때 배포된 환경이 http 인지 https 인지 잘 확인 해야 합니다.
브라우저의 보안상 http로 배포를 했을 경우 위치 추적이 허용되지 않습니다.
실 서비스에서 사용 하실 분들은 https로 변환 한다면 localhost에서 작동했던 것처럼 잘 작동 할 것입니다!

지도 생성

1. index.html 파일에 script 구문을 넣는다.

<Head>
  <script
    src={`https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=${process.env.NEXT_PUBLIC_MAP_KEY}&callback=CALLBACK_FUNCTION`}
    defer={false}
  ></script>
</Head>

react 같은 경우는 public/index.html 파일에 넣지만,
Next에서는 JSX 구문에서 <Head></Head> 안에 script구문을 넣을 수 있습니다.

앞으로 우리는 MapContainer 파일을 생성후 지도가 동작할 기능 로직을 넣어줍니다.

2. jsx 구문에 id값 넣어주기

<MapLayout id="map">{!loading && <Loader />}</MapLayout>

JSX에 id가 map 이라는 div를 작성해줍니다.

이제부터 map이라는 id 를 추적하여 지도는 MapLayOut 안에 그려지게 됩니다.

지도를 생성하려면 꼭 MapLayOut 안에 width, height를 꼭 넣어 줘야 지도가 그려집니다.

3. 지도 생성 로직 만들기

const mapRef = useRef<HTMLElement | null | any>(null);

useEffect(() => {
   if (typeof myLocation !== 'string')
      mapRef.current = new naver.maps.Map('map', {
	center: new naver.maps.LatLng(myLocation.latitude,myLocation.longitude),
        zoomControl: true,
      });
}, [mapRef, myLocation]);

우리는 map을 생성하여 여러 커스텀을 하고, 마커도 생성하고 map에게 많은 기능을 줄 것이기 때문에, 특정 dom에 다가갈 수 있는 useRef를 생성합니다.

지도는 컴포넌트가 렌더링 되고, 딱 한번 그려야 되기 때문에 useEffect 안에서 생성합니다.
이때 생성되는 조건은 myLocation이 초기값('string')이 아닐때 실행 되는 것입니다.

생성된 지도는 mapRef.current 안에 담아 둘 것입니다.

우리는 이제 new naver.maps 을 사용하여 지도와 관련된 모든 것을 mapRef.current을 사용하려합니다 😊

아래 사진과 같이 지도가 그려지게 됩니다.

지도 옵션 추가/제거

지도의 옵션을 추가하거나 제거 할 수 있습니다.

mapRef.current.setOption({
	zoomControl : false;
})

지도 왼쪽 상단에 뜨는 zoomControl이 거슬리는 분들은 옵션을 주어 지도를 자유롭게 커스텀 할 수 있습니다.

Naver Map docs 에서 option을 확인 할 수 있습니다.

https://navermaps.github.io/maps.js/docs/naver.maps.html#.MapOptions

지도에 마커 생성하기

const markerRef = useRef<any | null>(null);

useEffect(() => {
	markerRef.current = new naver.maps.Marker({
	  position: new naver.maps.LatLng(37.4979517, 127.0276188),
	  map: mapRef.current,
	  icon: {
	    content: [markerHtml("현재 위치")].join(''),
	    size: new naver.maps.Size(38, 58),
	    anchor: new naver.maps.Point(19, 58),
	  },
	});
}, [])

위와 같은 형식으로 marker를 생성 할 수 있습니다.
icon을 따로 넣지 않으면 네이버 기본 마커로 합니다.

하지만 저는 custom 이미지로 마커를 표현 할 것이기 때문에, icon 옵션을 추가해 주었고 markerHtml()이라는 html을 컴포넌트화 하여 사용할 것입니다. (markerHtml은 html 구문이여야 합니다)

우리는 mark를 가지고 다양한 이벤트를 생성해 줄 것이기 때문에 useRef를 사용하여 생성한 마커를 markerRef에 담아줍니다.

마커 여러개 생성하기

하나의 마커를 생성하는 것이 아닌, 여러 마커를 생성해야 하는 경우에는 배열로 담은 리스트를 준비합니다.

data?.map((item: ShopsListType) => {
  markerRef.current = new naver.maps.Marker({
    position: new naver.maps.LatLng(item?.map_y_location, item?.map_x_location),
    map: mapRef.current,
    icon: {
      content: [markerHtml(item.name)].join(''),
      size: new naver.maps.Size(38, 58),
      anchor: new naver.maps.Point(19, 58),
    },
  });
});

하나의 마커 생성하는 것과 같이 사용하면 됩니다.

마커 클릭시 부드럽게 지도 이동

지도에 있는 마커를 클릭시 자연스럽게 이동하는 것을 구현하려 합니다.

Naver Maps Api docs 를 참고하면 더욱 쉽게 개발 할 수 있습니다.

NAVER Maps API v3

Naver Maps 안에 panTo() 메서드를 사용하여 지도를 부드럽게 이동 시킬 것입니다.

panTo(coord, transitionOptions)을 정의합니다.

function markerClickEvent(marker: any, item: ShopsListType) {
	naver.maps.Event.addListener(marker, 'click', (e: any) => {
	      const mapLatLng = new naver.maps.LatLng(
	        Number(item?.map_y_location),
	        Number(item?.map_x_location)
	      );
	
	      // 선택한 마커로 부드럽게 이동합니다.
	      mapRef.current.panTo(mapLatLng, e?.coord);
	    }
	  });
	}
}

markerClickEvent() 함수를 생성해준 뒤 marker 를 클릭 했을 경우에 대한 코드를 작성합니다.

그럼 우리는 마커를 클릭했을때 지도가 자연스럽게 이동하는 것을 볼 수 있습니다.

마커에 하이라이트 넣기

클릭한 마커에 강조되게 마커 하이라이트를 넣을 수 있습니다.

const selectedMarker = useRef<any | null>(null); // 선택된 마커를 구분하기 위해 useRef 추가

function markerClickEvent(marker: any, item: ShopsListType) {
	naver.maps.Event.addListener(marker, 'click', (e: any) => {

		// 클릭된 마커가 없고, click된 마커가 클릭된 마커가 아니라면
 		// 마커의 이미지를 클릭 이미지로 변경합니다
		if (!selectedMarker.current ||(selectedMarker.current !== marker && name !== undefined)) {
		 
		  // 클릭된 마커 객체가 null이 아니면
		  // 클릭된 마커의 이미지를 기본 이미지로 변경합니다.
		  if (!!selectedMarker.current) {
		    selectedMarker.current.setIcon({
		      content: [markerHtml(selectedName || '지점')].join(''),
		      size: new naver.maps.Size(38, 58),
		      anchor: new naver.maps.Point(19, 58),
		    });
		  }
		
		  // 클릭했을때 마커에 하이라이트를 표시해줍니다.
		  marker.setIcon({
		    content: [clickedMarkerHtml(item.name)].join(''),
		    size: new naver.maps.Size(38, 58),
		    anchor: new naver.maps.Point(19, 58),
		  });
		
		  // 클릭된 마커를 현재 클릭된 마커로 설정합니다.
		  selectedMarker.current = marker;
	})
})

위에 코드를 markerClickEvent() 안에 넣어 작동시켜 줍니다.
저는 하이라이트로 아이콘을 변경 시킬 것이기 때문에 클릭하면 아이콘을 바꿔줍니다. marker의 아이콘을 변경 시켜 주는 메서드는 setIcon() 입니다.

사용된 메서드

  • setIcon()
  • naver.maps.Event.addListener(marker, 'click', 콜백함수)

발생했던 이슈

처음 마커를 클릭하고 다른 마커를 클릭하게 된다면 처음 클릭했던 마커의 이름이 undefiend로 바꼈습니다.
그래서 처음 클릭했던 이름을 useState에 담아보려 했지만 useEffect에서 작동하는 마커는 딱 한번만 생성되기 때문에 useState는 적용되지 않았습니다.

해결방법

  1. 클로저

클로저의 개념을 사용하여 해결하려고 했지만 클로저가 실행되려면 코드들이 여러번 실행 되어야 했습니다. 하지만 마커는 한번 찍어주는 것이기 때문에 여러번 실행되어 클릭하기 전의 값을 담을 수 없었습니다.

  1. 데이터에서 선택한 마커를 찾아 선택된 마커의 이름을 가져와 넣어주자
const selectedName = beginData?.find(
  (di: ShopsListType) => di.marker === selectedMarker.current
)?.name;

위와 같은 코드를 추가해준 뒤에 이름을 가져와 넘겨주니 이전에 클릭했던 마커의 이름이 undefiend가 아닌 정확한 이름을 가져 올 수 있도록 해결했습니다.

리스트 아이템 클릭시 부드럽게 지도 이동

지도와 서로 관련이 없는 사이드에 있는 리스트 아이템을 클릭하면 지도가 움직여야 된다는 요구사항이 있었습니다.

naver map이 제공하는 panTo() 메서드는 marker의 click event가 발생했을때라는 조건을 가지고 있습니다. 그래서 왼쪽에 있는 상점 리스트를 클릭하더라도 마커가 클릭한 것과 같이 지도를 움직이는 구조를 만들어야 했습니다.
하지만, 네이버 지도는 map script안에서 일어난 것만 이벤트 처리를 할 수 있는 예외 사항이 있었습니다. 상점 리스트는 map script와 전혀 상관없는 컴포넌트이기 때문에 해결방법을 찾지 못하고 있었습니다.

마커를 생성할때 item 안에 marker라는 것을 생성하고 마커에다가 생성한 marker 을 담아주는 구조를 만들어 준후 ListItem을 클릭하면 marker.trigger(”click”)을 걸어주어 지도 안에 있는 marker을 클릭한 것과 같은 효과를 낼 수 있었습니다.

스토리보드

  1. Marker을 생성할 때, item.marker = markerRef.current를 담아줍니다.
useEffect(() => {
	function createMarker() {
	  beginData?.map((item: ShopsListType) => {
	    markerRef.current = new naver.maps.Marker({
	      position: new naver.maps.LatLng(
	        Number(item?.map_y_location),
	        Number(item?.map_x_location)
	      ),
	      map: mapRef.current,
	      icon: {
	        content: [markerHtml(item.name)].join(''),
	        size: new naver.maps.Size(38, 58),
	        anchor: new naver.maps.Point(19, 58),
	      },
	    });
	
	    markerClickEvent(markerRef.current, item);
	
	    item.marker = markerRef.current; // 담아준다.
	  });
	}
})

코드에서 marker.current = new naver.maps.Marker() 을 생성하고,
item.marker에 생성한 marker.curren 를 담아두는 것을 볼 수 있습니다.

  1. shopsList 데이터에서 클릭하려는 리스트의 id를 찾은후 찾은 데이터에서 marker로 접근 한 다음 지도 안에 있는 마커를 클릭한 같은 역할을 하기 위하여 "click" trigger을 걸어줍니다.
const markerMove = () => {
  shopsList
    ?.find((i: ShopsListType) => i.id === item.id)
    ?.marker?.trigger('click');
};
  1. ListItem 을 클릭하면 마커를 클릭한 것과 같은 동작을 하게 됩니다.

최종 결과물

6개의 댓글

comment-user-thumbnail
2022년 11월 15일

정말 잘 만드셨네요 ㅎㅎ, 많이 배웠습니다! 궁금한게 있는데 마커에 텍스트를 html로 넣으신건가요?

1개의 답글
comment-user-thumbnail
2022년 11월 30일

정말 도움 많이 됐습니다! 혹시 이것도 Git 공유 해주실 수 있나요!?

답글 달기
comment-user-thumbnail
2023년 1월 9일

정말 도움 많이 됐습니다!😊👍
감사합니다!

1개의 답글
comment-user-thumbnail
2023년 7월 29일

안녕하세요. 글 도움이 많이 됐습니다. markerHtml() 에서 아이콘과 html 작성은 어떻게 하셨는지 궁금한데 공유 부탁드려도 될까요?

답글 달기