배달의 민족 웹 프로젝트에서 지도를 구현했던 것에 대해 이야기해보려고 한다.
배달받을 장소를 선택할 수 있는 페이지에서 구현할 지도 기능은 다음과 같다.
- 사용자가 해당 페이지에 접근하면, 현재 위치가 지도와 주소 검색창에 표시
- 다음 우편번호 서비스를 사용하여 주소를 검색하고, 선택한 주소의 위치가 지도와 주소 검색창에 표시
- 지도 클릭시 클릭한 부분의 위치에 마커가 표시되고 해당 주소가 주소 검색창에 표시
- 현재 위치로 이동하는 버튼 클릭시 지도와 주소 검색창에 표시
<!-- 다음 우편번호 서비스 API를 불러오는 스크립트 -->
<script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
다음 우편주소 API는 주소 검색 기능을 제공하나, 지도에서 마커를 찍기 위한 위도, 경도 정보를 제공하지 않기 때문에 주소로부터 위도, 경도를 얻는 추가적인 API가 필요 -> 네이버 지도 API의 Geocode 서비스를 함께 사용
https://www.ncloud.com/ (네이버 클라우드 플랫폼에 접속) -> 콘솔 클릭
Services -> AI NAVER API 클릭 -> Application 등록
Application 이름을 입력한 후 밑에 그림처럼 체크해준다. 여기서 Geocoding과 Reverse Geocoding을 선택해야 주소를 좌표로 변환하거나 좌표를 주소로 변환하는 것이 가능하다.
Web 서비스에 이용할 것이기 때문에 We
b 서비스 URL부분에 해당 페이지의 URL을 입력해준다.
등록을 하면 다음과 같이 인증 정보를 볼 수 있다.
<!-- 네이버 지도 API를 불러오는 스크립트 -->
<script type="text/javascript"
src="https://openapi.map.naver.com/openapi/v3/maps.js?ncpClientId=**인증 받은 키**&submodules=geocoder"></script>
// 주소 검색창에서 주소 선택 시
const changeInputText = () => {
new daum.Postcode({
oncomplete: function (data) {
const addr = data.address; // 선택한 주소
// 주소로 네이버 지도 검색 및 표시
searchAddressdaum(addr); // 주소로 좌표 검색하여 지도에 표시
},
}).open();
};
- 사용자 입력 단순화: 사용자가 직접 주소를 입력하지 않고, 편리하게 선택
- 지도 서비스 연동: 선택한 주소를 기반으로 좌표 검색 및 지도 표시같은 추가적인 작업 수행
// 주소를 검색하여 지도에 표시하고 마커 생성(다음주소api에서 사용할 함수)
const searchAddressdaum = (address) => {
naver.maps.Service.geocode(
{
query: address, // 검색할 주소
},
(status, response) => {
// 응답 상태가 OK인 경우에만 처리
if (
status === naver.maps.Service.Status.OK &&
response.v2.addresses.length > 0
) {
const result = response.v2.addresses[0]; // 첫 번째 검색 결과
const lat = result.y; // 위도
const lng = result.x; // 경도
// 좌표로 지도와 마커 표시
const latlng = new naver.maps.LatLng(lat, lng);
// 지도 중심을 검색된 위치로 이동 (기존 지도 객체 사용)
if (map) {
map.setCenter(latlng); // 기존 지도에서 중심만 변경
} else {
// 지도 객체가 없다면 새로 생성
map = new naver.maps.Map("map", {
center: latlng,
zoom: 17,
});
}
// 기존 마커가 있으면 삭제
if (currentMarker) {
currentMarker.setMap(null);
}
// 새 마커 생성
currentMarker = new naver.maps.Marker({
position: latlng, // 마커 위치
map: map, // 마커가 표시될 지도
title: address, // 마커의 타이틀
});
// 화면에 주소 표시
addAddress(address);
} else {
alert("주소 검색에 실패했습니다.");
}
}
);
};
<div>
의 id
- 중복 방지: 입력된 주소를 기반으로 좌표 검색 후, 이전 마커를 갱신하여 지도에 표시
- 사용자 경험 향상: 검색 결과가 없을 경우 오류 메시지를 표시
// 지도 부분
let map; // 전역 변수로 map 객체 관리(재사용)
let currentMarker = null; // 전역 변수로 마커 관리
// 지도 생성 및 현재 위치로 이동
const moveCurrnet = () => {
// 위치 정보를 얻기 위해 Geolocation API 사용
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(
(position) => {
// 사용자의 현재 위치 가져오기
const lat = position.coords.latitude;
const lon = position.coords.longitude;
// 지도 생성 (처음 한 번만 생성)
if (!map) {
map = new naver.maps.Map("map", {
center: new naver.maps.LatLng(lat, lon), // 현재 위치로 지도의 중심 설정
zoom: 17, // 초기 줌 레벨 설정
minZoom: 7, // 최소 줌 레벨
zoomControl: true, // 줌 컨트롤 표시
zoomControlOptions: {
position: naver.maps.Position.TOP_RIGHT,
},
});
// 지도 클릭 시 마커를 찍고 주소를 받아오기
naver.maps.Event.addListener(map, "click", (e) => {
const latlng = e.coord; // 클릭한 좌표
// 기존 마커가 있으면 삭제
if (currentMarker) {
currentMarker.setMap(null);
}
// 새로운 마커 생성
currentMarker = new naver.maps.Marker({
position: latlng, // 클릭한 위치에 마커 생성
map: map, // 마커가 표시될 지도
title: "클릭한 위치", // 마커의 제목
});
map.panTo(latlng); // 지도 이동
// 클릭한 위치의 주소를 받아오기
getAddressFromLatLng(latlng, addAddress);
});
} else {
map.setCenter(new naver.maps.LatLng(lat, lon)); // 기존 지도 중심 재설정
}
// 현재 위치에 마커 표시 (기존 마커가 있으면 삭제 후 갱신)
if (currentMarker) {
currentMarker.setMap(null); // 이전 마커 제거
}
currentMarker = new naver.maps.Marker({
position: new naver.maps.LatLng(lat, lon),
map: map,
title: "현재 위치",
});
// 현재 위치의 주소를 받아오기
getAddressFromLatLng(new naver.maps.LatLng(lat, lon), addAddress);
},
(error) => {
let errorMessage = "";
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage =
"위치 정보를 허용해야 합니다. 위치 서비스 권한을 부여해 주세요.";
break;
case error.POSITION_UNAVAILABLE:
errorMessage =
"위치 정보를 사용할 수 없습니다. 네트워크 상태나 GPS 신호를 확인해 주세요.";
break;
case error.TIMEOUT:
errorMessage =
"위치 정보를 가져오는데 시간이 너무 걸렸습니다. 다시 시도해 주세요.";
break;
default:
errorMessage = "알 수 없는 오류가 발생했습니다.";
break;
}
alert(errorMessage);
}
);
} else {
alert("이 브라우저는 위치 정보를 지원하지 않습니다.");
}
};
- 동적 지도 관리: 지도 객체와 마커를 효율적으로 관리하여 불필요한 리소스 낭비 방지
- 현재 위치 기반 지도 표시: 사용자 경험 향상을 위해 현재 위치를 기반으로 지도 초기화
- 사용자 인터랙션 지원: 사용자가 클릭한 위치에 마커 생성 및 해당 위치의 주소 확인
- 사용자 경험 개선: 클릭 이벤트를 활용해 실시간으로 지도와 주소 업데이트
- 포괄적인 오류 처리: 다양한 오류 상황에 대한 구체적인 메세지 제공
// 주소로부터 현재 위치를 얻어오기 (Reverse Geocoding)
const getAddressFromLatLng = (latlng, callback) => {
naver.maps.Service.reverseGeocode(
{
location: latlng,
},
(status, response) => {
if (status === naver.maps.Service.Status.OK) {
const address = response.result.items[0].address; // 가장 가까운 주소
callback(address);
} else {
callback("위치를 찾지 못했습니다.");
}
}
);
};
콜백 함수 활용: 주소를 받아 화면에 표시하거나 다른 작업을 처리할 수 있도록 유연성 제공
// 주소를 화면에 표시하고 로컬 스토리지에 저장
const addAddress = (address) => {
document.getElementById("address").value = address;
window.localStorage.setItem("name", address);
};
- 피드백: 선택한 주소를 UI에 바로 반영
- 데이터 지속성: 브라우저 세션 간 데이터 유지 -> 이전에 선택한 주소를 새 페이지에 자동으로 불러와 입력 필드에 표시
// 현재 위치 버튼 클릭 시
document.getElementById("showMap").addEventListener("click", function (e) {
e.preventDefault();
moveCurrnet();
});
- 지도에서 현재 위치 버튼을 누르거나 주소 검색시 지도에서 이동하지 않음(map 객체가 두 번 생성되어 문제 발생)
-> 이미 존재하는 map객체를 재사용하여 객체의 중심만 변경- map 객체가 처음 생성되거나 이미 존재할 때 모두 지도 클릭 이벤트가 제대로 처리되도록 하고 싶음
-> moveCurrnet 함수에서 처음 지도 객체를 생성할 때, 지도 클릭 이벤트 리스너를 추가하고 지도 객체가 이미 존재한다면 기존 객체에 이벤트 리스너를 추가
-> 클릭한 위치에 마커를 추가하는 로직을 클릭 이벤트 리스너 안에서 처리- 주소를 검색해서 추가한 마커도 기존 마커와 함께 남아있음
-> 기존에 표시된 마커가 있다면 setMap(null)을 호출하여 삭제하고, 새로운 마커만 currentMarker에 추가