1편에서는 api 키 발급, 지도 제작하기까지 완료했다.
2편에서는 현재 사용자 위치 받기, 마커와 정보창 만들기, 클릭이벤트 설정, 지도 영역 내 마커만 보이기 등의 기능을 정리할 예정이다.
이제 내 위치를 받아와보자. 사용자 위치는 Geolocation API를 이용해서 받아올 수 있다.
Geolocation API 사용하기
자세한 내용은 공식 문서에서 확인하고 바로 코드를 작성해보자.
정리하면 경로가 다음과 같다. src/hooks/useGeolocation.js
여기에 다음과 같이 코드를 작성한다.
import { useState, useEffect } from 'react';
const useGeoloaction = () => {
const [currentMyLocation, setCurrentMyLocation] = useState({
lat: 0,
lng: 0,
});
const [locationLoading, setLocationLoading] = useState(false);
const getCurPosition = () => {
setLocationLoading(true);
const success = (location) => {
setCurrentMyLocation({
lat: location.coords.latitude,
lng: location.coords.longitude,
});
setLocationLoading(false);
};
const error = () => {
setCurrentMyLocation({ lat: 37.5666103, lng: 126.9783882 });
setLocationLoading(false);
};
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(success, error);
}
};
useEffect(() => {
getCurPosition();
}, []);
return { currentMyLocation, locationLoading, getCurPosition };
};
export default useGeoloaction;
이제 앞서 만들었던 지도에 사용자 위치를 넣어보자.
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
pc를 이용한 웹 이기에 다소 오차가 많이 있을수도 있지만 기존에 설정한 서울 시청이 아닌 현재 위치와 근접하게 지도가 나타날 것이다.
이제 마커를 만들어보자.
마커는 new naver.maps.Marker를 이용해 만들 수 있다.
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
// 현재 내 위치 마커 표시
new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
현재는 기본 마커를 썼는데 다음과 같이 마커를 커스텀 해줄수도 있다.
const markerContent = `
<div style="border: 1px solid black; border-radius: 50%; width: 20px; height: 20px"></div>
`;
// 현재 내 위치 마커 표시
new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
icon: {
content: markerContent,
anchor: new naver.maps.Point(0, 50), // 마커의 위치 설정
},
});
이외에도 더 다양한 옵션이 있고, 이미지로 마커를 대체할 수도 있으니 원하는 방식으로 본인의 마커를 커스텀하기 바란다.
일단, 설명은 기본 마커로 계속 진행하겠다.
정보창은 new naver.maps.InfoWindow를 이용해 만들 수 있다.
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
// 현재 내 위치 마커 표시
new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
// 정보창 객체
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;">여기는 제목</div>`,
` <div style="font-size: 13px;">여기는 내용<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
아마 정보창이 안 나올 것으로 예상하지만 다양한 시도를 해보지 않아서 자세히는 모르겠다. 바로 나온다면 다행일수도 있고...
아무튼, 일반적으로 정보창은 마커와 결합해서 '클릭이벤트'를 연결한다.
코드를 아래와 같이 조금 더 추가해주자.
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
// 현재 내 위치 마커 표시
const marker = new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
// 정보창 객체
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;">여기는 제목</div>`,
` <div style="font-size: 13px;">여기는 내용<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
// 현재 나와 가장 가까이 있는 화장실의 정보창 이벤트 핸들러
naver.maps.Event.addListener(marker, "click", () => {
if (infoWindow.getMap()) {
// 정보창이 닫힐 때 이벤트 발생
infoWindow.close();
} else if (mapRef.current !== null) {
// 정보창이 열릴 때 이벤트 발생
infoWindow.open(mapRef.current, marker);
}
});
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
보면 new naver.maps.Marker를 marker라는 변수에 할당해주고 addListener에 해당 marker와 infoWindow를 연결해 클릭이벤트를 설정하였다.
마커를 클릭하면 우리가 설정한 정보창이 나타나고 마커를 다시 클릭하면 정보창이 닫힐 것이다.
이제 조금만 더 난이도를 높여서 마커와 정보창을 여러 개로 설정해보자.
코드가 길어져서 슬슬 알아보기 어려울 것 같아 추가되는 코드를 먼저 설명하겠다.
// 마커 리스트와 정보창 리스트 선언
const markers = [];
const infoWindows = [];
const samples = [
{ lat: currentMyLocation.lat, lng: currentMyLocation.lng },
{ lat: 37.5666103, lng: 126.9783882 },
{ lat: 37.5796103, lng: 126.9772882 },
]; // 좌표 샘플
for (let i = 0; i < samples.length; i++) {
// 현재 내 위치 마커 표시
const marker = new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
// 정보창 객체
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;">여기는 제목${i+1}</div>`,
` <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
markers.push(marker);
infoWindows.push(infoWindow);
}
// 각 마커에 이벤트가 발생했을 때 기능 설정
const getClickHandler = (index) => {
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));
}
샘플은 내 현재위치, 서울 시청, 경복궁이다. 완성된 코드는 아래와 같다.
이제 완성 코드를 확인하자!
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
// 마커 리스트와 정보창 리스트 선언
const markers = [];
const infoWindows = [];
const samples = [
{ lat: currentMyLocation.lat, lng: currentMyLocation.lng },
{ lat: 37.5666103, lng: 126.9783882 },
{ lat: 37.5796103, lng: 126.9772882 },
]; // 좌표 샘플
for (let i = 0; i < samples.length; i++) {
// 현재 내 위치 마커 표시
const marker = new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
// 정보창 객체
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;">여기는 제목${i+1}</div>`,
` <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
markers.push(marker);
infoWindows.push(infoWindow);
}
// 각 마커에 이벤트가 발생했을 때 기능 설정
const getClickHandler = (index) => {
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));
}
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
완성된 코드로 실행해보면 총 3개의 마커가 나타날 것이다.(운이 안 좋게 사용자 위치가 서울 시청이나 경복궁과 겹친다면 2개가 나타날 것이다)
각 마커를 클릭하면 각각의 정보창이 나타나고 리스트 번호를 볼 수 있을 것이다.
마지막으로 마커가 내가 현재 보는 지도의 영역 밖으로 나가면 사라지는 기능을 추가하겠다.
이 기능은 아래 참고자료를 작성하신 donggu님께 감사함을 전하며 자세한 설명은 참고자료를 꼭 확인하길 바란다.
네이버 지도 api를 이용하여 지도 만들기
새로운 파일을 만들어야 한다.
// 마커 표시 함수
const showMarker = (map, marker) => {
marker.setMap(map);
};
// 마커 숨김 함수
const hideMarker = (marker) => {
marker.setMap(null);
};
const checkForMarkersRendering = (map, markers) => {
const mapBounds = map.getBounds();
for (let i = 0; i < markers.length; i += 1) {
const position = markers[i].getPosition();
if (mapBounds.hasLatLng(position)) {
showMarker(map, markers[i]);
} else {
hideMarker(markers[i]);
}
}
};
export default checkForMarkersRendering;
순서대로 잘 따라왔으면 마지막으로 Map.jsx를 수정해주자.
import { useEffect, useRef } from 'react';
import useGeolocation from '../../hooks/useGeolocation'; // 본인 폴더 위치에 주의할 것!
import checkForMarkersRendering from '../../util/checkForMarkersRendering';
function Map() {
const mapRef = useRef(null);
const { naver } = window;
const { currentMyLocation } = useGeolocation();
useEffect(() => {
if (currentMyLocation.lat !== 0 && currentMyLocation.lng !== 0) {
// 네이버 지도 옵션 선택
const mapOptions = {
// 지도의 초기 중심 좌표
center: new naver.maps.LatLng(currentMyLocation.lat, currentMyLocation.lng),
logoControl: false, // 네이버 로고 표시 X
mapDataControl: false, // 지도 데이터 저작권 컨트롤 표시 X
scaleControl: true, // 지도 축척 컨트롤의 표시 여부
tileDuration: 200, // 지도 타일을 전환할 때 페이드 인 효과의 지속 시간(밀리초)
zoom: 14, // 지도의 초기 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: { position: 9 }, // 줌 컨트롤 우하단에 배치
};
mapRef.current = new naver.maps.Map(
'map',
mapOptions
);
// 마커 리스트와 정보창 리스트 선언
const markers = [];
const infoWindows = [];
const samples = [
{ lat: currentMyLocation.lat, lng: currentMyLocation.lng },
{ lat: 37.5666103, lng: 126.9783882 },
{ lat: 37.5796103, lng: 126.9772882 },
]; // 좌표 샘플
for (let i = 0; i < samples.length; i++) {
// 현재 내 위치 마커 표시
const marker = new naver.maps.Marker({
// 생성될 마커의 위치
position: new naver.maps.LatLng(samples[i].lat, samples[i].lng),
// 마커를 표시할 Map 객체
map: mapRef.current,
});
// 정보창 객체
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;">여기는 제목${i+1}</div>`,
` <div style="font-size: 13px;">여기는 내용${i+1}<div>`,
"</div>",
].join(""),
maxWidth: 300,
anchorSize: {
width: 12,
height: 14,
},
borderColor: "#cecdc7",
});
markers.push(marker);
infoWindows.push(infoWindow);
}
// 각 마커에 이벤트가 발생했을 때 기능 설정
const getClickHandler = (index) => {
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.maps.Event.addListener(mapRef.current, "zoom_changed", () => {
if (mapRef.current !== null) {
checkForMarkersRendering(mapRef.current, markers);
}
});
// 지도 드래그 시 마커 업데이트 이벤트 핸들러
naver.maps.Event.addListener(mapRef.current, "dragend", () => {
if (mapRef.current !== null) {
checkForMarkersRendering(mapRef.current, markers);
}
});
}
}, [currentMyLocation]);
return <div id="map" />
}
export default Map;
이로써 이번 프로젝트에서 배운 네이버지도 api 다루는 법을 모두 정리했다.
시간 문제와 두 번째 프로젝트로 인해 자세하게 정리하지는 못 했지만, 본질적인 목표인 불친절한 네이버 공식문서에 애먹는 다른 예비 개발자 분들이 조금이라도 더 쉽게 api를 다뤘으면 해서 되도록 완성본 코드로 작성하였다.
물론, 공식문서를 보지말라는 얘기는 아니다. 바닐라 JS 시절을 기준으로 문서가 작성되어 있어 react에 변경하여 적용하는 방법이 어려울 뿐이지 공식문서에는 지금 내 설명보다 더 많은 기능들이 기록되어 있다.
당장은 해석하기 어렵겠지만 내 코드를 바탕으로 공식문서를 열심히 해석해서 더 멋진 지도를 만들길 바란다.