[JavaScript] 지도 구현하기

태연·2025년 2월 15일
0

프로젝트

목록 보기
3/4
post-thumbnail

배달의 민족 웹 프로젝트에서 지도를 구현했던 것에 대해 이야기해보려고 한다.



배달받을 장소를 선택할 수 있는 페이지에서 구현할 지도 기능은 다음과 같다.

  • 사용자가 해당 페이지에 접근하면, 현재 위치가 지도와 주소 검색창에 표시
  • 다음 우편번호 서비스를 사용하여 주소를 검색하고, 선택한 주소의 위치가 지도와 주소 검색창에 표시
  • 지도 클릭시 클릭한 부분의 위치에 마커가 표시되고 해당 주소가 주소 검색창에 표시
  • 현재 위치로 이동하는 버튼 클릭시 지도와 주소 검색창에 표시



필요한 API

다음 우편번호 서비스 API

<!-- 다음 우편번호 서비스 API를 불러오는 스크립트 -->
  <script src="//t1.daumcdn.net/mapjsapi/bundle/postcode/prod/postcode.v2.js"></script>
  • 주소 검색을 제공하는 서비스
  • 사용자가 주소를 검색하고 선택 가능
  • 주소 검색 결과로 위도와 경도를 직접 제공하지 않음
    - 별도의 API를 이용하여 위도, 경도를 구해야 함

다음 우편주소 API는 주소 검색 기능을 제공하나, 지도에서 마커를 찍기 위한 위도, 경도 정보를 제공하지 않기 때문에 주소로부터 위도, 경도를 얻는 추가적인 API가 필요 -> 네이버 지도 API의 Geocode 서비스를 함께 사용


네이버 지도 API

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>
  • 네이버 클라우드 플랫폼에서 발급받은 클라이언트 ID를 인증 받은 키 부분에 입력
  • 기본적인 지도 렌더링과 기본 기능 사용 가능
  • submodules=geocoder로 주소를 좌표로 변환하거나 좌표를 주소로 변환할 때 필요한 Geocoder 모듈을 추가로 불러옴


구현

주소 검색창에서 주소 선택

// 주소 검색창에서 주소 선택 시
const changeInputText = () => {
  new daum.Postcode({
    oncomplete: function (data) {
      const addr = data.address; // 선택한 주소
      // 주소로 네이버 지도 검색 및 표시
      searchAddressdaum(addr); // 주소로 좌표 검색하여 지도에 표시
    },
  }).open();
};
  • 다음 우편번호 서비스를 사용하여 주소 검색
  • new daum.Postcode().open()으로 주소를 검색하고 선택할 수 있는 UI 제공
    - daum.Postcode는 주소 검색을 위한 API를 호출
  • 주소 선택시 oncomplete 콜백이 실행되면서 선택한 주소인 data.address를 가져옴
  • 해당 주소(addr)를 활용하여 searchAddressdaum 함수에서 네이버 지도에 표시
    - 다음 우편번호 API로 주소를 선택 -> 해당 주소의 위도와 경도를 네이버 지오코딩 API로 변환하여 사용
  1. 사용자 입력 단순화: 사용자가 직접 주소를 입력하지 않고, 편리하게 선택
  2. 지도 서비스 연동: 선택한 주소를 기반으로 좌표 검색 및 지도 표시같은 추가적인 작업 수행


검색한 주소를 지도에 표시

// 주소를 검색하여 지도에 표시하고 마커 생성(다음주소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("주소 검색에 실패했습니다.");
      }
    }
  );
};
  • 네이버 지도 API를 사용해 주소를 검색하고, 검색된 주소를 지도에 표시
    - 다음 우편번호 서비스 API와 네이버 지도 API 연동
  • 입력한 주소를 naver.maps.Service.geocode로 전달해 좌표 검색
  • 검색 결과(response.v2.addresses[0])에서 첫 번째 주소 데이터 가져와 검색 성공시 위도(lat)와 경도(lng) 정보를 추출
  • 지도 객체가 있으면 중심 좌표를 업데이트(map.setCenter(latlng))
  • 기존 지도 객체(map)가 없다면 new naver.maps.Map("map", {...})새로 생성
    - "map"은 HTML에서 지도를 표시할 <div>의 id
  • 기존 마커가 있으면 setMap(null)로 삭제 후 new naver.maps.Marker({...})로 새 마커 생성
  • addAddress 함수를 통해 주소 검색창에 해당 주소 한글로 표시
  1. 중복 방지: 입력된 주소를 기반으로 좌표 검색 후, 이전 마커를 갱신하여 지도에 표시
  2. 사용자 경험 향상: 검색 결과가 없을 경우 오류 메시지를 표시


현재 위치를 지도에 표시 및 클릭한 위치의 주소 얻어오는 기능

// 지도 부분
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("이 브라우저는 위치 정보를 지원하지 않습니다.");
  }
};
  • 네이버 지도 API와 Geolocation API를 사용
  • navigator.geolocation.getCurrentPosition를 사용하여 현재 위치(위도/경도) 가져옴
    - 가져오면 처음에는 지도를 생성하고, 이후엔 지도 중심만 갱신
    • 기존 마커 제거 후 현재 위치에 새로운 마커 표시
    • getAddressFromLatLng 함수로 위도/경도를 이용해 해당 위치의 주소를 가져옴
      -네이버 지도 API에서 Reverse Geocoding 기능을 통해 수행
  • 지도 클릭시
    - 클릭한 좌표에 새로운 마커 생성 및 기존 마커 제거
    - 클릭한 좌표로 지도 중심 이동
    • 클릭한 위치의 주소를 얻어 화면에 표시
  1. 동적 지도 관리: 지도 객체와 마커를 효율적으로 관리하여 불필요한 리소스 낭비 방지
  2. 현재 위치 기반 지도 표시: 사용자 경험 향상을 위해 현재 위치를 기반으로 지도 초기화
  3. 사용자 인터랙션 지원: 사용자가 클릭한 위치에 마커 생성 및 해당 위치의 주소 확인
  4. 사용자 경험 개선: 클릭 이벤트를 활용해 실시간으로 지도와 주소 업데이트
  5. 포괄적인 오류 처리: 다양한 오류 상황에 대한 구체적인 메세지 제공


주소로부터 현재 위치 얻어오기

// 주소로부터 현재 위치를 얻어오기 (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("위치를 찾지 못했습니다.");
      }
    }
  );
};
  • 네이버 지도 API의 Reverse Geocoding 기능을 사용하여 주어진 좌표로부터 주소를 추출
    - 좌표로부터 사람이 읽을 수 있는 주소를 얻기 위한 기능
  • naver.maps.Service.reverseGeocode를 사용하여 좌표(latlng)로부터 주소 검색
  • 상태 코드가 OK인 경우
    - response.result.items[0].address를 통해 가장 가까운 주소를 가져옴
    • 해당 주소를 callback함수에 전달
  • 상태 코드가 OK가 아닌 경우
    - 메세지를 callback함수로 전달

    콜백 함수 활용: 주소를 받아 화면에 표시하거나 다른 작업을 처리할 수 있도록 유연성 제공



주소를 주소 검색창에 표시 및 로컬스토리지에 저장

// 주소를 화면에 표시하고 로컬 스토리지에 저장
const addAddress = (address) => {
  document.getElementById("address").value = address;
  window.localStorage.setItem("name", address);
};
  • 지도에서 클릭하거나 현재 위치를 검색한 경우 해당 주소를 UI와 로컬스토리지에 동기화
  1. 피드백: 선택한 주소를 UI에 바로 반영
  2. 데이터 지속성: 브라우저 세션 간 데이터 유지 -> 이전에 선택한 주소를 새 페이지에 자동으로 불러와 입력 필드에 표시


현재 위치로 이동 버튼

// 현재 위치 버튼 클릭 시
document.getElementById("showMap").addEventListener("click", function (e) {
  e.preventDefault();
  moveCurrnet();
});
  • 현재 위치 버튼에 리스너를 추가해 클릭하면 현재 위치로 이동하는 moveCurrent 함수 호출


결과



직면한 문제와 해결방안

  • 지도에서 현재 위치 버튼을 누르거나 주소 검색시 지도에서 이동하지 않음(map 객체가 두 번 생성되어 문제 발생)
    -> 이미 존재하는 map객체를 재사용하여 객체의 중심만 변경
  • map 객체가 처음 생성되거나 이미 존재할 때 모두 지도 클릭 이벤트가 제대로 처리되도록 하고 싶음
    -> moveCurrnet 함수에서 처음 지도 객체를 생성할 때, 지도 클릭 이벤트 리스너를 추가하고 지도 객체가 이미 존재한다면 기존 객체에 이벤트 리스너를 추가
    -> 클릭한 위치에 마커를 추가하는 로직을 클릭 이벤트 리스너 안에서 처리
  • 주소를 검색해서 추가한 마커도 기존 마커와 함께 남아있음
    -> 기존에 표시된 마커가 있다면 setMap(null)을 호출하여 삭제하고, 새로운 마커만 currentMarker에 추가
profile
어서와

0개의 댓글