본문 바로가기

React

카카오 맵 javascript api 적용하기 - 키워드 검색

728x90

참고 가이드 : https://apis.map.kakao.com/web/sample/keywordList/

 

기존에 정보를 입력할때 다음 주소 api를 사용했지만 키워드 검색으로 수정했다.

가이드 문서에 javscript + html + css가 다 되어있어서 이를 활용하기로 했다.

 

map을 렌더링할 div

 

<div className="keyword_wrap">
	<div id="map" style={{width:"57vw", height:"60vh", position:"relative", overflow:"hidden"}}></div>

	<div id="menu_wrap" className="bg_white">
		<div className="option">
			<div>
				키워드 : <input type="text" id="keyword" size="15" /> 
				<button onClick={searchPlaces}>검색하기</button> 
			</div>
		</div>
		<hr></hr>
		<ul id="placesList"></ul>
		<div id="pagination"></div>
	</div>
</div>

 

 

리액트를 하면서 아직 미숙해서인지 useState로 사용하다 보면 비동기 문제로 생각대로 관리가 안될때가 있었다.

그래서 전역변수 선언

 

var markers = [];
var ps;
var infowindow;
var map;

 

useEffect에 사용할 맵을 그리는 함수 drawKeywordMap

 

const drawKeywordMap = () => {
	var mapContainer = document.getElementById('map'), // 지도를 표시할 div 
	mapOption = {
      center: new kakao.maps.LatLng(37.566826, 126.9786567), // 지도의 중심좌표
      level: 3 // 지도의 확대 레벨
    };  

	// 지도를 생성합니다    
	map = new kakao.maps.Map(mapContainer, mapOption); 

	// 장소 검색 객체를 생성합니다
	ps = new kakao.maps.services.Places();  
	// 검색 결과 목록이나 마커를 클릭했을 때 장소명을 표출할 인포윈도우를 생성합니다
	infowindow = new kakao.maps.InfoWindow({zIndex:1});
}

 

검색 이벤트 함수

 

const searchPlaces = () => {
        var keyword = document.getElementById('keyword').value;

        if(isBrowser){
            if (!keyword.replace(/^\s+|\s+$/g, '')) {
                alert('키워드를 입력해주세요!');
                return false;
            }
        }
    
        // 장소검색 객체를 통해 키워드로 장소검색을 요청합니다
        ps.keywordSearch( keyword, placesSearchCB); 
    }

 

검색 이벤트 함수로 ps.keywordSearch함수를 실행하고 콜백함수인 placesSearchCB로 검색 후 로직을 정의해준다.

 

검색 이벤트 콜백 placesSearchCB

 

const placesSearchCB = (data, status, pagination) => {
        if (status === kakao.maps.services.Status.OK) {

            // 정상적으로 검색이 완료됐으면
            // 검색 목록과 마커를 표출합니다
            displayPlaces(data);

            // 페이지 번호를 표출합니다
            if(isBrowser){
                displayPagination(pagination);
            }

        } else if (status === kakao.maps.services.Status.ZERO_RESULT && isBrowser) {

            alert('검색 결과가 존재하지 않습니다.');
            return;

        } else if (status === kakao.maps.services.Status.ERROR && isBrowser) {

            alert('검색 결과 중 오류가 발생했습니다.');
            return;

        }
    }

첫번째 매개변수인 data에 검색결과 배열형태로 넘어온다

모바일에서는 페이징, 알럿처리를 하지 않기 때문에 isBrowser 사용으로 디바이스 구분을 해주었다.

(react-device-detect 패키지)

 

마커 표시, 마커 이벤트 함수 displayPlaces

const displayPlaces = (places) => {
        var listEl = document.getElementById('placesList'), 
        menuEl = document.getElementById('menu_wrap'),
        fragment = document.createDocumentFragment(), 
        bounds = new kakao.maps.LatLngBounds(), 
        listStr = '';
        
        // 검색 결과 목록에 추가된 항목들을 제거합니다
        if(isBrowser){
            removeAllChildNods(listEl);
        }

        // 지도에 표시되고 있는 마커를 제거합니다
        removeMarker();
        
        for ( var i=0; i<places.length; i++ ) {
            // 마커를 생성하고 지도에 표시합니다
            var placePosition = new kakao.maps.LatLng(places[i].y, places[i].x);
            var marker = addMarker(placePosition, i);
            var itemEl = getListItem(i, places[i]); // 검색 결과 항목 Element를 생성합니다

            // 검색된 장소 위치를 기준으로 지도 범위를 재설정하기위해
            // LatLngBounds 객체에 좌표를 추가합니다
            bounds.extend(placePosition);

            // 마커와 검색결과 항목에 mouseover 했을때
            // 해당 장소에 인포윈도우에 장소명을 표시합니다
            // mouseout 했을 때는 인포윈도우를 닫습니다
            (function(marker, place) {
                var name = place.place_name

                kakao.maps.event.addListener(marker, 'mouseover', function() {
                    displayInfowindow(marker, name);
                });

                kakao.maps.event.addListener(marker, 'mouseout', function() {
                    infowindow.close();
                });
                kakao.maps.event.addListener(marker, 'click', function() {
                    setShop({...shop, address : place.road_address_name, coordX : place.y, coordY : place.x});
                });

                if(isMobile){
                    displayInfowindow(marker, name);
                }

                if(isBrowser){
                    itemEl.onmouseover =  function () {
                        displayInfowindow(marker, name);
                    };

                    itemEl.onmouseout =  function () {
                        infowindow.close();
                    };
                    itemEl.onclick = function () {
                        setShop({...shop, address : place.road_address_name});
                    }
                }
            })(marker, places[i]);

            if(isBrowser){
                fragment.appendChild(itemEl);
            }
        }

        if(isBrowser){
            // 검색결과 항목들을 검색결과 목록 Elemnet에 추가합니다
            listEl.appendChild(fragment);
            menuEl.scrollTop = 0;
        }
        // 검색된 장소 위치를 기준으로 지도 범위를 재설정합니다
        map.setBounds(bounds);
    }

places로 검색 결과 리스트가 넘어오고 for문으로 이벤트처리, append를 하고있다.

추가적으로 마커 클릭시 해당 결과의 도로명 주소를 setState하는 이벤트를 추가했다.

 

place에는 대략 위도, 경도, 지번주소, 도로명주소 이름 등이 담겨있다.

 

 

(function(x2, y2){

   ...

})(x1, y1);

위처럼 생긴 구문은 x1 => x2,  y1 => y2로 매칭된다.

 

 

그 외 마커 생성 삭제 및 함수들은 가이드문서 그대로 사용했다.

const getListItem = (index, places) => {
        var el = document.createElement('li'),
        itemStr = '<span class="markerbg marker_' + (index+1) + '"></span>' +
                    '<div class="info">' +
                    '   <h5>' + places.place_name + '</h5>';

        if (places.road_address_name) {
            itemStr += '    <span>' + places.road_address_name + '</span>' +
                        '   <span class="jibun gray">' +  places.address_name  + '</span>';
        } else {
            itemStr += '    <span>' +  places.address_name  + '</span>'; 
        }
                    
        itemStr += '  <span class="tel">' + places.phone  + '</span>' +
                    '</div>';           

        el.innerHTML = itemStr;
        el.className = 'item';

        return el;
    }

    // 마커를 생성하고 지도 위에 마커를 표시하는 함수입니다
    const addMarker = (position, idx) => {
        var imageSrc = 'https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png', // 마커 이미지 url, 스프라이트 이미지를 씁니다
            imageSize = new kakao.maps.Size(36, 37),  // 마커 이미지의 크기
            imgOptions =  {
                spriteSize : new kakao.maps.Size(36, 691), // 스프라이트 이미지의 크기
                spriteOrigin : new kakao.maps.Point(0, (idx*46)+10), // 스프라이트 이미지 중 사용할 영역의 좌상단 좌표
                offset: new kakao.maps.Point(13, 37) // 마커 좌표에 일치시킬 이미지 내에서의 좌표
            },
            markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imgOptions),
            marker = new kakao.maps.Marker({
                position: position, // 마커의 위치
                image: markerImage 
            });

        marker.setMap(map); // 지도 위에 마커를 표출합니다
        markers.push(marker);  // 배열에 생성된 마커를 추가합니다

        return marker;
    }

    // 지도 위에 표시되고 있는 마커를 모두 제거합니다
    const removeMarker = () => {
        for ( var i = 0; i < markers.length; i++ ) {
            markers[i].setMap(null);
        }   
        markers = [];
    }

    // 검색결과 목록 하단에 페이지번호를 표시는 함수입니다
    const displayPagination = (pagination) => {
        var paginationEl = document.getElementById('pagination'),
            fragment = document.createDocumentFragment(),
            i; 

        // 기존에 추가된 페이지번호를 삭제합니다
        while (paginationEl.hasChildNodes()) {
            paginationEl.removeChild (paginationEl.lastChild);
        }

        for (i=1; i<=pagination.last; i++) {
            var el = document.createElement('a');
            //el.href = "#";
            el.innerHTML = i;

            if (i===pagination.current) {
                el.className = 'on';
            } else {
                el.onclick = (function(i) {
                    return function() {
                        pagination.gotoPage(i);
                    }
                })(i);
            }

            fragment.appendChild(el);
        }
        paginationEl.appendChild(fragment);
    }

    // 검색결과 목록 또는 마커를 클릭했을 때 호출되는 함수입니다
    // 인포윈도우에 장소명을 표시합니다
    const displayInfowindow = (marker, title) => {
        var content = '<div style="padding:5px;z-index:1;">' + title + '</div>';
        infowindow.setContent(content);
        infowindow.open(map, marker);
    }

    // 검색결과 목록의 자식 Element를 제거하는 함수입니다
    const removeAllChildNods = (el) => {   
        while (el.hasChildNodes()) {
            el.removeChild (el.lastChild);
        }
    }

 

 

PC 결과

 

 

MO 결과

모바일의 경우에 이벤트 처리를 잘해주어야 한다.

728x90