지도는 현재 내 위치를 중심으로 생성되어야 한다. 그래서 지도 생성 전에 먼저 사용자의 현재 위치를 불러와야 하는데, 자바스크립트에서 제공하고 있는 Geolocation API
를 사용해서 불러 올 수 있다.
사용자의 현재 위치는 getCurrentPosition()
메서드로 가져올 수 있다. 이 메서드는 첫 번째 파라미터로 위치 요청 성공 시 실행할 콜백 함수를 받고, 두 번째 파라미터로는 위치 요청 실패 시 실행할 콜백 함수를 받는다.
아래의 예제는 위치 요청 성공 시 사용자의 현재 위치를 state에 담고, 실패 시 서울시청의 위치를 담아주도록 했다.
const setCurrentMyLocation = useSetRecoilState(currentMyLocationAtom);
useEffect(() => {
// 내 현재 위치 값 번환 성공 시 실행 함수 -> 내 현재 위치 값을 currentMyLocationAtom에 저장
const success = (location: { coords: { latitude: number; longitude: number } }) => {
setCurrentMyLocation({
lat: location.coords.latitude,
lng: location.coords.longitude,
});
};
// 내 현재 위치 값 반환 실패 시 실행 함수 -> 지도 중심을 서울시청 위치로 설정
const error = () => {
setCurrentMyLocation({ lat: 37.5666103, lng: 126.9783882 });
};
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(success, error);
}
}, [setCurrentMyLocation]);
기본적인 지도는 Naver Maps API에서 제공하는 naver.maps.Map
클래스로 생성할 수 있다.
Map
클래스는 애플리케이션에서 지도 인스턴스를 정의한다. 이 객체를 생성함으로써 지정한 DOM 요소에 지도를 삽입할 수 있다.
new naver.maps.Map(mapDiv, mapOptions)
- Parameters
- mapDiv : 지도를 삽입할 HTML 요소 또는 HTML 요소의 id
- mapOptions : 지도의 옵션 객체
- Properties
- controls : 사전에 정의된 지도 내의 위치별로 지도 컨트롤의 인스턴스를 포함하는 객체. 사용자 정의 컨트롤을 이 속성 내 특정 위치에 추가함으로써 사용자 정의 컨트롤을 지도에 추가할 수 있음
- data : 데이터 레이어를 정의하는 Data 객체
- layers : 지도 레이어의 컬렉션을 포함하는 객체
- mapTypes : 지도 유형의 컬렉션을 포함하는 객체
- mapSystemProjection : 프로젝션 객체. 지도 좌표와 세계 좌표, 화면 픽셀 좌표 간 좌표를 변환할 수 있는 메서드 제공
map에 계속 접근해야 하기 때문에 DOM에 접근 가능한 useRef를 생성한다.
지도는 한 번만 렌더링 되어야 하기 때문에 useEffect 안에서 생성하고, new naver.maps.Map
의 첫번째 파라미터로 지도를 삽입할 HTML요소의 id인 map
과 두번째 파라미터로 지도의 각 옵션들을 설정해서 지도를 생성한다.
const mapRef = useRef<naver.maps.Map | null>(null);
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 현재 내 위치를 중심으로 하는 지도 생성
mapRef.current = new naver.maps.Map("map", {
// 지도 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 지도 초기 줌 레벨
zoom: 15,
// 지도 최소 줌 레벨
minZoom: 10,
// 줌 컨트롤 표시 여부
zoomControl: true,
// 지도 유형 컨트롤 표시 여부
mapTypeControl: true,
// 줌 컨트롤의 옵션
zoomControlOptions: {
// 줌 컨트롤의 위치를 우측 상단으로 배치함
position: naver.maps.Position.TOP_RIGHT,
},
// 지도 데이터 저작권 컨트롤 표시 여부
mapDataControl: false,
});
}
}, [currentMyLocation]);
...
return (
<>
<MapContainer id='map'></MapContainer>
</>
);
참고
LatLng 클래스는 위도, 경도 좌표를 정의한다.
위에서 지도를 생성할 때 center 옵션을 설정 시 new naver.maps.LatLng
클래스를 사용한다.
new naver.maps.LatLng(lat, lng)
- Parameters
- lat : 위도 (기본값 0)
- lng : 경도 (기본값 0)
Marker 클래스는 지도 위에 표시되는 마커를 정의한다.
new naver.maps.Map
는 파라미터로 마커의 옵션을 파라미터로 받는데, 이때 position 속성은 필수로 설정해 주어야 하며, 이 마커가 표시될 지도인 map: mapRef.current
를 설정해 준다.
그리고 별도의 마커 아이콘을 지정해주지 않으면 네이버 기본 마커로 생성되며, icon
프로퍼티로 사용자 지정 마커로 변경할 수 있다.
new naver.maps.Marker(options)
- Parameters
- options : 마커 옵션. 이때 position 속성은 반드시 설정해야 한다.
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 현재 내 위치를 중심으로 하는 지도 생성
mapRef.current = new naver.maps.Map("map", {
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
zoom: 15,
minZoom: 10,
zoomControl: true,
mapTypeControl: true,
zoomControlOptions: {
position: naver.maps.Position.TOP_RIGHT,
},
logoControl: false,
mapDataControl: false,
});
// 현재 내 위치 마커 표시
new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
// 마커의 모양
icon: {
url: `${myMarker}`,
size: new naver.maps.Size(43, 43),
scaledSize: new naver.maps.Size(43, 43),
},
// 마커의 쌓임 순서
zIndex: 999,
});
}
}, [currentMyLocation, setIsMapLoading]);
// 마커들이 담겨있는 배열
const markers: naver.maps.Marker[] = [];
// 내 현재 위치에서 가장 가까운 화장실 100개만 마커 생성
for (let i = 1; i <= 100; i++) {
const marker = new naver.maps.Marker({
map: mapRef.current,
position: new naver.maps.LatLng(sortedToiletData[i].Y_WGS84, sortedToiletData[i].X_WGS84),
icon: {
url: `${aroundToilet}`,
size: new naver.maps.Size(35, 35),
scaledSize: new naver.maps.Size(35, 35),
},
});
markers.push(marker);
}
참고
이 기능은 마커 클릭 시 클릭한 마커가 가지고 있는 정보(주소, 건물명 등)를 말풍선으로 표시할 수 있는 기능이다.
new naver.maps.InfoWindow
는 파라미터로 정보창의 옵션을 파라미터로 받는데, contents 프로퍼티에 정보창에 표시될 내용을 html로 생성해준다.
그리고 addListener
를 이용하여 대상 마커에서 이벤트 알림을 받아 정보창을 호출하는 리스너를 등록한다.
new naver.maps.InfoWindow(options)
- Parameters
- options : 정보 창 옵션
<static>
addListener(target, eventName, listener)
- Parameters
- target : 이벤트 대상 객체
- eventName : 이벤트 이름
- listenr : 이벤트 리스너
// 현재 나와 가장 가까이 있는 화장실의 정보창 생성
const infoWindow = new naver.maps.InfoWindow({
content: [
'<div style="padding: 10px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 16px 0px;">',
` <div style="font-weight: bold; margin-bottom: 5px;">${sortedToiletData[0].FNAME}</div>`,
` <div style="font-size: 13px;">${sortedToiletData[0].ANAME}<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
// 현재 나와 가장 가까이 있는 화장실의 정보창 이벤트 핸들러
naver.maps.Event.addListener(closetMarker, "click", () => {
if (infoWindow.getMap()) {
// 정보창이 닫힐 때 이벤트 발생
infoWindow.close();
} else if (mapRef.current !== null) {
// 정보창이 열릴 때 이벤트 발생
infoWindow.open(mapRef.current, closetMarker);
}
});
// 나머지 화장실 마커의 인덱스를 클로저 변수로 저장하는 이벤트 핸들러를 리턴하는 함수
const getClickHandler = (index: number) => {
return () => {
if (infoWindows[index].getMap()) {
infoWindows[index].close();
} else if (mapRef.current !== null) {
infoWindows[index].open(mapRef.current, markers[index]);
}
};
};
// 나머지 각 화장실의 정보창 이벤트 핸들러
for (let i = 0; i < markers.length; i++) {
naver.maps.Event.addListener(markers[i], "click", getClickHandler(i));
}
참고
이 기능은 표시되지 않는 부분의 마커는 지도에서 제거하여 최적화 할 수 있는 기능이다.
naver.mapse.Marker
클래스의 메서드 중 하나인 setMap()
으로 마커가 지도 위에 있으면 마커를 표시하고, 없다면 지도에서 숨기는 함수를 각각 만들어준다.
// 마커 표시 함수
const showMarker = (map: naver.maps.Map, marker: naver.maps.Marker) => {
marker.setMap(map);
};
// 마커 숨김 함수
const hideMarker = (marker: naver.maps.Marker) => {
marker.setMap(null);
};
그리고 지도와 마커를 파라미터로 받아 마커를 숨길지 표시할지 판별하는 함수를 만들어 준다. 여기서 마커가 지도에서 벗어나 있는지를 판단하기 위해 naver.maps.Map
의 getBounds()
메서드로 현재 보이는 지도 화면의 좌표 경계를 불러온다.
각 마커들을 순회하며 마커의 위치를 반환하는 naver.maps.Marker
클래스의 getPosition()
메서드로 i
번째 마커의 위치를 확인한다. 이후 지도의 좌표 경계 내에 지정한 마커가 있는지를 리턴하는 naver.maps.LatLngBounds
클래스의 hasLatLng()
메서드를 이용하여 해당 마커가 지도 내에 있다면 showMarker
함수를 실행하고, 지도 밖에 있다면 hideMarker
함수를 실행한다.
// 마커 업데이트 유/무 판별 함수
const updateMarkers = (map: naver.maps.Map, markers: naver.maps.Marker[]) => {
const mapBounds : any = map.getBounds();
for (let i = 0; i < markers.length; i++) {
const position = markers[i].getPosition();
if (mapBounds.hasLatLng(position)) {
showMarker(map, markers[i]);
} else {
hideMarker(map, markers[i]);
}
}
};
// 지도 줌 인/아웃 시 마커 업데이트 이벤트 핸들러
naver.maps.Event.addListener(mapRef.current, "zoom_changed", () => {
if (mapRef.current !== null) {
updateMarkers(mapRef.current, markers);
}
});
// 지도 드래그 시 마커 업데이트 이벤트 핸들러
naver.maps.Event.addListener(mapRef.current, "dragend", () => {
if (mapRef.current !== null) {
updateMarkers(mapRef.current, markers);
}
});
참고