본문 바로가기
정보기술

(5) 구글 지도 API - 자동완성주소 검색하기

by 엘라트리니티 2022. 12. 14.

2022.12.14 - [정보기술] - (1) 구글 지도 API – 지도 만들기

2022.12.14 - [정보기술] - (2) 구글 지도 API - 단일 마커 표시하기

2022.12.14 - [정보기술] - (3) 구글 지도 API - 다중 마커 표시하기

2022.12.14 - [정보기술] - (4) 구글 지도 API - 정보창 표시하기

2022.12.14 - [정보기술] - (5) 구글 지도 API - 자동완성주소 검색하기

2022.12.14 - [정보기술] - (6) 번외 - 자동완성주소 검색에 관하여

2022.12.14 - [정보기술] - (7) 구글 지도 API - 특정 주소의 좌표 확인하기

2022.12.14 - [정보기술] - (8) 구글 지도 API - 좌표 DB 등록하기

2022.12.14 - [정보기술] - (9) 구글 지도 API - 검색 인터페이스 구현

2022.12.14 - [정보기술] - (10) 번외 - 검색지점 기준 반경거리 계산 (하버사인 공식)

2022.12.14 - [정보기술] - (11) 구글 지도 API - 좌표 DB 불러오기 (xml 이용)

2022.12.14 - [정보기술] - (12) 구글 지도 API - 위치기반 서비스 제공 (끝)

 

다음은 자동완성주소 검색하기 입니다. 이 기능은 위치기반 서비스의 핵심이라고 할 수 있습니다. 서비스 운영자 입장에서는 특정 위치에 대한 정보를 DB에 기록할 때 사용하며 서비스 이용자 입장에서는 특정 위치에 대한 정보를 얻을 때 사용됩니다. 예제를 확인하기 위한 접근 메뉴는 [Samples] – [Places] – [Autocomplete] 입니다.

구글 지도 API : Place Autocomplete 예제

 

자동완성 서비스 자체는 이미 구글이나 네이버 등에서 키워드를 검색할 때 많이 접해보셨으리라 생각됩니다. 단어나 초성등을 입력하면 서버 측에서 해당 조건에 맞는 키워드 정보를 보내오는 것이죠. 자동완성에 대한 이야기를 하자면 지면이 길어질 거 같아서 다른 포스팅에서 다뤄보도록 하고 먼저 스크립트 전문을 살펴보겠습니다.

 

<!DOCTYPE html>
<html>
  <head>
    <title>Place Autocomplete</title>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <style>
      /* Always set the map height explicitly to define the size of the div
       * element that contains the map. */
      #map {
        height: 100%;
      }
      /* Optional: Makes the sample page fill the window. */
      html, body {
        height: 100%;
        margin: 0;
        padding: 0;
      }
      #description {
        font-family: Roboto;
        font-size: 15px;
        font-weight: 300;
      }

      #infowindow-content .title {
        font-weight: bold;
      }

      #infowindow-content {
        display: none;
      }

      #map #infowindow-content {
        display: inline;
      }

      .pac-card {
        margin: 10px 10px 0 0;
        border-radius: 2px 0 0 2px;
        box-sizing: border-box;
        -moz-box-sizing: border-box;
        outline: none;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
        background-color: #fff;
        font-family: Roboto;
      }

      #pac-container {
        padding-bottom: 12px;
        margin-right: 12px;
      }

      .pac-controls {
        display: inline-block;
        padding: 5px 11px;
      }

      .pac-controls label {
        font-family: Roboto;
        font-size: 13px;
        font-weight: 300;
      }

      #pac-input {
        background-color: #fff;
        font-family: Roboto;
        font-size: 15px;
        font-weight: 300;
        margin-left: 12px;
        padding: 0 11px 0 13px;
        text-overflow: ellipsis;
        width: 400px;
      }

      #pac-input:focus {
        border-color: #4d90fe;
      }

      #title {
        color: #fff;
        background-color: #4d90fe;
        font-size: 25px;
        font-weight: 500;
        padding: 6px 12px;
      }
    </style>
  </head>
  <body>
    <div class="pac-card" id="pac-card">
      <div>
        <div id="title">
          Autocomplete search
        </div>
        <div id="type-selector" class="pac-controls">
          <input type="radio" name="type" id="changetype-all" checked="checked">
          <label for="changetype-all">All</label>

          <input type="radio" name="type" id="changetype-establishment">
          <label for="changetype-establishment">Establishments</label>

          <input type="radio" name="type" id="changetype-address">
          <label for="changetype-address">Addresses</label>

          <input type="radio" name="type" id="changetype-geocode">
          <label for="changetype-geocode">Geocodes</label>
        </div>
        <div id="strict-bounds-selector" class="pac-controls">
          <input type="checkbox" id="use-strict-bounds" value="">
          <label for="use-strict-bounds">Strict Bounds</label>
        </div>
      </div>
      <div id="pac-container">
        <input id="pac-input" type="text"
            placeholder="Enter a location">
      </div>
    </div>
    <div id="map"></div>
    <div id="infowindow-content">
      <img src="" width="16" height="16" id="place-icon">
      <span id="place-name"  class="title"></span><br>
      <span id="place-address"></span>
    </div>

    <script>
      // This example requires the Places library. Include the libraries=places
      // parameter when you first load the API. For example:
      // <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places">

      function initMap() {
        var map = new google.maps.Map(document.getElementById('map'), {
          center: {lat: 37.5546788, lng: 126.9706069},
          zoom: 13
        });
        var card = document.getElementById('pac-card');
        var input = document.getElementById('pac-input');
        var types = document.getElementById('type-selector');
        var strictBounds = document.getElementById('strict-bounds-selector');

        map.controls[google.maps.ControlPosition.TOP_RIGHT].push(card);

        var autocomplete = new google.maps.places.Autocomplete(input);

        // Bind the map's bounds (viewport) property to the autocomplete object,
        // so that the autocomplete requests use the current map bounds for the
        // bounds option in the request.
        autocomplete.bindTo('bounds', map);

        // Set the data fields to return when the user selects a place.
        autocomplete.setFields(
            ['address_components', 'geometry', 'icon', 'name']);

        var infowindow = new google.maps.InfoWindow();
        var infowindowContent = document.getElementById('infowindow-content');
        infowindow.setContent(infowindowContent);
        var marker = new google.maps.Marker({
          map: map,
          anchorPoint: new google.maps.Point(0, -29)
        });

        autocomplete.addListener('place_changed', function() {
          infowindow.close();
          marker.setVisible(false);
          var place = autocomplete.getPlace();
          if (!place.geometry) {
            // User entered the name of a Place that was not suggested and
            // pressed the Enter key, or the Place Details request failed.
            window.alert("No details available for input: '" + place.name + "'");
            return;
          }

          // If the place has a geometry, then present it on a map.
          if (place.geometry.viewport) {
            map.fitBounds(place.geometry.viewport);
          } else {
            map.setCenter(place.geometry.location);
            map.setZoom(17);  // Why 17? Because it looks good.
          }
          marker.setPosition(place.geometry.location);
          marker.setVisible(true);

          var address = '';
          if (place.address_components) {
            address = [
              (place.address_components[0] && place.address_components[0].short_name || ''),
              (place.address_components[1] && place.address_components[1].short_name || ''),
              (place.address_components[2] && place.address_components[2].short_name || '')
            ].join(' ');
          }

          infowindowContent.children['place-icon'].src = place.icon;
          infowindowContent.children['place-name'].textContent = place.name;
          infowindowContent.children['place-address'].textContent = address;
          infowindow.open(map, marker);
        });

        // Sets a listener on a radio button to change the filter type on Places
        // Autocomplete.
        function setupClickListener(id, types) {
          var radioButton = document.getElementById(id);
          radioButton.addEventListener('click', function() {
            autocomplete.setTypes(types);
          });
        }

        setupClickListener('changetype-all', []);
        setupClickListener('changetype-address', ['address']);
        setupClickListener('changetype-establishment', ['establishment']);
        setupClickListener('changetype-geocode', ['geocode']);

        document.getElementById('use-strict-bounds')
            .addEventListener('click', function() {
              console.log('Checkbox clicked! New state=' + this.checked);
              autocomplete.setOptions({strictBounds: this.checked});
            });
      }
    </script>
    <script src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&libraries=places&callback=initMap"
        defer></script>
  </body>
</html>

 

기존 스크립트와 다르게 정말 기네요. 200줄 정도 됩니다. 

CSS도 복잡한 편이라 실제 서비스를 구현하실 때에 다른 디자인으로 적용하시려면 이것저것 손대야 할 부분이 많습니다. 중요한 부분인 스크립트부터 확인해보겠습니다. 워낙 길다보니 하나하나 잘라서 보도록 하겠습니다.

 

function initMap() {
        var map = new google.maps.Map(document.getElementById('map'), {
          center: {lat: 37.5546788, lng: 126.9706069},
          zoom: 13
        });
        var card = document.getElementById('pac-card');
        var input = document.getElementById('pac-input');
        var types = document.getElementById('type-selector');
        var strictBounds = document.getElementById('strict-bounds-selector');

        map.controls[google.maps.ControlPosition.TOP_RIGHT].push(card);

        var autocomplete = new google.maps.places.Autocomplete(input);
        
        // Bind the map's bounds (viewport) property to the autocomplete object,
        // so that the autocomplete requests use the current map bounds for the
        // bounds option in the request.
        autocomplete.bindTo('bounds', map);

        // Set the data fields to return when the user selects a place.
        autocomplete.setFields(
            ['address_components', 'geometry', 'icon', 'name']);

 

익숙한 부분입니다.

서울역을 중심으로 하여 지도를 초기화하고 Zoom 레벨은 13입니다. 아래에 var로 선언되는 4가지의 변수들은 해당 JSP 문서(document)내의 ID가 ‘pac-card’, ‘pac-input’, ‘type-selector’, ‘strict-bounds-select’인 입력값을 불러와서 각 변수로 선언하겠다는 뜻입니다.

아래의 map.controls는 구글 지도 API에서 제공되는 controls이라는 기능을 구현한 것으로 보이구요 var autocomplete는 input 값을 기반으로 구글 지도 API에서 제공하는 자동완성기능을 사용한다는 것을 의미합니다.

input 값은 유저가 실행한 장소에 대한 검색값을 의미합니다. 직후 바로 아래의 주석을 번역하면 지도의 경계 (뷰포트) 속성을 자동 완성 객체에 바인딩하고 자동 완성 요청이 현재지도 경계를 사용하도록 요청옵션을 제한하는 것입니다.

 

  var infowindow = new google.maps.InfoWindow();
        var infowindowContent = document.getElementById('infowindow-content');
        infowindow.setContent(infowindowContent);
        var marker = new google.maps.Marker({
          map: map,
          anchorPoint: new google.maps.Point(0, -29)
        });

 

이후 정보창(infowindow)에 대한 변수 선언을 합니다.

이 때 document내의 ID ‘infowindow-content’의 값을 setcontent하겠다는 구문이네요. Marker에 대해서 anchorPoint를 설정해놨는데 이건 마커가 입력되는 위도/경도에 대한 정보가 아닙니다.

바로 infowindow가 마커 위에 표시되는(걸리는=anchor) 위치에 대한 설정이라고 보시면 됩니다. 실제로 해당 구문이 없이도 주소검색은 정상적으로 작동합니다. 그러나 해당 포인트가 가장 자연스러운 marker상의 infowindow의 위치라고 구글에서는 제안합니다.

다음은 자동완성 (autocomplete)에 대한 함수입니다. 단일함수이기 때문에 따로 나누진 않고 순서대로 설명드리도록 하겠습니다.

 

autocomplete.addListener('place_changed', function() {
          infowindow.close();
          marker.setVisible(false);
          var place = autocomplete.getPlace();
          if (!place.geometry) {
            // User entered the name of a Place that was not suggested and
            // pressed the Enter key, or the Place Details request failed.
            window.alert("No details available for input: '" + place.name + "'");
            return;
          }

          // If the place has a geometry, then present it on a map.
          if (place.geometry.viewport) {
            map.fitBounds(place.geometry.viewport);
          } else {
            map.setCenter(place.geometry.location);
            map.setZoom(17);  // Why 17? Because it looks good.
          }
          marker.setPosition(place.geometry.location);
          marker.setVisible(true);

          var address = '';
          if (place.address_components) {
            address = [
              (place.address_components[0] && place.address_components[0].short_name || ''),
              (place.address_components[1] && place.address_components[1].short_name || ''),
              (place.address_components[2] && place.address_components[2].short_name || '')
            ].join(' ');
          }

          infowindowContent.children['place-icon'].src = place.icon;
          infowindowContent.children['place-name'].textContent = place.name;
          infowindowContent.children['place-address'].textContent = address;
          infowindow.open(map, marker);
        });

        // Sets a listener on a radio button to change the filter type on Places
        // Autocomplete.
        function setupClickListener(id, types) {
          var radioButton = document.getElementById(id);
          radioButton.addEventListener('click', function() {
            autocomplete.setTypes(types);
          });
        }

        setupClickListener('changetype-all', []);
        setupClickListener('changetype-address', ['address']);
        setupClickListener('changetype-establishment', ['establishment']);
        setupClickListener('changetype-geocode', ['geocode']);

        document.getElementById('use-strict-bounds')
            .addEventListener('click', function() {
              console.log('Checkbox clicked! New state=' + this.checked);
              autocomplete.setOptions({strictBounds: this.checked});
            });
      }

 

먼저 autocomplete.addListener는 자동완성주소 검색이 실행되었을 때 발생하는 이벤트입니다. ‘place_changed’라는 구글 지도 API의 선택값으로 작동하며 function 이하의 내용들을 실행합니다.

다음 infowindow.close()로 열려있는 정보창들을 닫아줍니다. 또한 열려있는 marker가 있다면 setvisible(false)로 없애주고 유저가 입력한 키워드를 이용하여 자동완성 검색을 실시합니다.

이 때 적절한 장소가 없다면 alert으로 경고창을 띄웁니다. 아무런 값을 입력하지 않거나 자동완성목록에서 선택하지 않을 경우 경고창이 작동합니다.

유저가 검색한 장소가 존재한다면.. 먼저 viewport가 존재한다면 해당값을 지도의 경계값으로 설정합니다.

viewport가 존재하지 않는다면 검색장소의 위도/경도값을 지도의 중앙값으로 설정합니다. 뷰포트에 대한 내용도 지도를 표시하는데 중요한 요소이니 따로 관련문서에서 찾아보시는 것을 권장드립니다.

이후 마커의 위치를 나타내고 (setposition) 지도 상에 노출 (setvisible.true) 합니다. 주소는 address 변수를 통해 나타나게 되는데 구글 서버에 저장된 주소배열중 앞에서 3가지 만을 가져옵니다. ([0], [1], [2])

만약 모든 정보가 존재할 경우 각 배열에 저장되는 값들을 다음과 같습니다.

 

대전 유성구 기준
0 : 봉명로 93 [로] Street 또는 [지번] Streetnumber
1 : 봉명동 [동] Neighborhood
2 : 유성구 [구/군] County
3 : 대전광역시 [시] City
4 : KR [국가] National
5 : 305-301 [우편번호] Postnumber


해당 배열은 절대적이지 않기 때문에 도로명주소나 지번이 존재하지 않을 경우, 일부 정보가 빠진 상태로 출력되는 경우가 있습니다. 따라서 각 배열들을 따로따로 활용하기 보다는 묶어서 지정하는 것이 좋으며 구글 예제에서는 이미 각 배열들을 address 변수에 통합하여 활용하고 있습니다.

infowindowContent.children은 자식요소를 html로 나타내는 방식이며 마커를 클릭했을 때의 정보창에 입력될 내용들을 나타냅니다. 아이콘과 장소이름, 주소가 있네요. 실제로 마커를 클릭하면 해당 값들이 나타나는 것을 확인할 수 있습니다.

이후 아래의 변수들은 장소검색시 옵션으로 제공되는 인터페이스를 선택하였을때 실행되는 기능에 대한 내용으로 생략하도록 하겠습니다. 주소를 검색하는 것에 대한 내용뿐이라면 이미 서술한 것으로 충분합니다.

여기까지 오셨으면 궁금증이 하나 생기셨을 텐데요.

  • 그러면 그 중요한 위도, 경도 정보가 빠져있지 않느냐?
  • lat 변수와 lng 변수는 지도 초기화 할때만 보이는데 어떻게된거냐?

라고 의문이 생기실 겁니다.

해답은 marker.setPosition(place.geometry.location)이라는 구문에 있습니다.

구글 지도 API에서 place.geometry.location에는 특정 장소에 대한 위도와 경도 정보가 담겨있다고 보시면 됩니다. 따라서 별도의 마커 위치에 대한 lat, lng값 설정없이 저 구문만으로도 마커가 검색된 장소에 가서 찍히게 되는 것이죠.

하지만 앞선 포스팅에서 말씀드렸던 것처럼 위치정보 서비스를 제공하려면 결국 장소에 대한 위도/경도 정보가 꼭 필요하고 이를 자동완성검색과 결합하는 방법이 요구되게 되는데 해당 방법은 이후 포스팅에서 다루도록 하겠습니다.

 

2022.12.14 - [정보기술] - (6) 번외 - 자동완성주소 검색에 관하여

 

(6) 번외 - 자동완성주소 검색에 관하여

2022.12.14 - [정보기술] - (1) 구글 지도 API – 지도 만들기 2022.12.14 - [정보기술] - (2) 구글 지도 API - 단일 마커 표시하기 2022.12.14 - [정보기술] - (3) 구글 지도 API - 다중 마커 표시하기 2022.12.14 - [정보

iftraveler.tistory.com

 

댓글