Never give up

React - Kakaomap example 본문

WEB

React - Kakaomap example

대기만성 개발자 2022. 2. 19. 16:29
반응형

오늘은 간단하게 react에서 kakao map을 만들어봤습니다

 

먼저 세팅부분은 가이드에 나와있는대로 해주고

(링크 : https://apis.map.kakao.com/web/guide/)

 

카카오맵 웹 예제에 나와있는 키워드 목록을 포함한 부분으로 테스트를 해봤습니다

(링크 : https://apis.map.kakao.com/web/sample/keywordList/)

 

Kakaomap example.js

/*global kakao*/
import React, { Component } from 'react'
import './kakaomap_example.css'

class KakaoMapExample extends Component {
    constructor() {
        super()

        this.markers = []
        this.map = null
        this.ps = null
        this.infowindow = null
        this.keyword = ''
        this.isCalled = false
    }

    componentDidMount() {
        this.script = document.createElement('script')

        this.script.async = true
        this.script.src = `https://dapi.kakao.com/v2/maps/sdk.js?autoload=false&libraries=services&appkey=${process.env.REACT_APP_KAKAO_MAP_KEY}`
        document.head.appendChild(this.script)

        this.script.onload = () => {
            kakao.maps.load(() => {
                this.loadOption()
            })
        }
    }

    componentWillUnmount(){
        document.head.removeChild(this.script)
    }

    loadOption = () => {
        const mapContainer = document.getElementById('map')

        const mapOption = {
            center: new kakao.maps.LatLng(33.450701, 126.570667),
            level: 3
        };

        this.map = new kakao.maps.Map(mapContainer, mapOption);

        const mapTypeControl = new kakao.maps.MapTypeControl();

        this.map.addControl(mapTypeControl, kakao.maps.ControlPosition.TOPRIGHT);

        const zoomControl = new kakao.maps.ZoomControl();
        this.map.addControl(zoomControl, kakao.maps.ControlPosition.RIGHT);

        this.ps = new kakao.maps.services.Places();
        this.infowindow = new kakao.maps.InfoWindow({ zIndex: 1 });
    }

    searchPlaces = () => {
        if (!this.keyword.replace(/^\s+|\s+$/g, '')) {
            alert('키워드를 입력해주세요!');
            return false;
        }

        this.ps.keywordSearch(this.keyword, this.placesSearchCB);
    }

    placesSearchCB = (data, status, pagination) => {
        if (this.isCalled) {
            return
        }

        this.isCalled = true

        setTimeout(() => this.isCalled = false, 3000)

        if (status === kakao.maps.services.Status.OK) {
            this.displayPlaces(data);

            this.displayPagination(pagination);
        } else if (status === kakao.maps.services.Status.ZERO_RESULT) {
            alert('검색 결과가 존재하지 않습니다.');
            return;
        } else if (status === kakao.maps.services.Status.ERROR) {
            alert('검색 결과 중 오류가 발생했습니다.');
            return;
        }
    }

    displayPlaces = (places) => {
        const listEl = document.getElementById('placesList')
        const menuEl = document.getElementById('menu_wrap')
        const fragment = document.createDocumentFragment()
        const bounds = new kakao.maps.LatLngBounds()

        this.removeAllChildNods(listEl);

        this.removeMarker();

        console.log({ places }, places.length)

        for (let i = 0; i < places.length; i++) {
            const placePosition = new kakao.maps.LatLng(places[i].y, places[i].x)
            const marker = this.addMarker(placePosition, i)
            const itemEl = this.getListItem(i, places[i]);

            bounds.extend(placePosition);

            const title = places[i].place_name

            kakao.maps.event.addListener(marker, 'mouseover', () => {
                this.displayInfowindow(marker, title);
            });

            kakao.maps.event.addListener(marker, 'mouseout', () => {
                this.infowindow.close();
            });

            itemEl.onmouseover = () => {
                this.displayInfowindow(marker, title);
            };

            itemEl.onmouseout = () => {
                this.infowindow.close();
            };

            fragment.appendChild(itemEl);
        }

        listEl.appendChild(fragment);
        menuEl.scrollTop = 0;

        this.map.setBounds(bounds);
    }

    getListItem = (index, places) => {
        const el = document.createElement('li')
        let 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;
    }

    addMarker = (position, idx, title) => {
        const imageSrc = 'https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png'
        const imageSize = new kakao.maps.Size(36, 37)
        const imgOptions = {
            spriteSize: new kakao.maps.Size(36, 691),
            spriteOrigin: new kakao.maps.Point(0, (idx * 46) + 10),
            offset: new kakao.maps.Point(13, 37)
        }
        const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize, imgOptions)

        const marker = new kakao.maps.Marker({
            position: position,
            image: markerImage
        });

        marker.setMap(this.map);
        this.markers.push(marker);

        return marker;
    }

    removeMarker = () => {
        this.markers.forEach((e, i) => {
            this.markers[i].setMap(null);
        })

        this.markers = [];
    }

    displayPagination = (pagination) => {
        const paginationEl = document.getElementById('pagination')
        const fragment = document.createDocumentFragment()

        while (paginationEl.hasChildNodes()) {
            paginationEl.removeChild(paginationEl.lastChild);
        }

        let elList = []

        for (let i = 1; i <= pagination.last; i++) {
            elList[i] = document.createElement('button')
            elList[i].innerHTML = i;

            if (i === pagination.current) {
                elList[i].className = 'on'
            }

            elList[i].onclick = async () => {
                elList.forEach((_,i) => {
                    elList[i].className = 'off'
                })

                elList[i].className = 'on'

                const res = await fetch(`https://dapi.kakao.com/v2/local/search/keyword.json?page=${i}&size=15&sort=accuracy&query=${this.keyword}`, {
                    headers: {
                        'Authorization': `KakaoAK ${process.env.REACT_APP_KAKAO_RESTAPI_KEY}`
                    }
                })

                const json = await res.json()

                this.displayPlaces(json['documents'])
            }

            fragment.appendChild(elList[i]);
        }
        paginationEl.appendChild(fragment);
    }

    displayInfowindow = (marker, title) => {
        const content = `<div style="padding:5px;z-index:1;">${title}</div>`

        this.infowindow.setContent(content);
        this.infowindow.open(this.map, marker);
    }

    removeAllChildNods = (el) => {
        while (el.hasChildNodes()) {
            el.removeChild(el.lastChild);
        }
    }

    render() {
        return (
            <>
                <div id='map' style={{ width: '100vw', height: '100vh' }} />
                <div id="menu_wrap" className="bg_white">
                    <div className="option">
                        <div>
                            키워드 : <input type="text" onChange={(e) => this.keyword = e.target.value} size="15" />
                            <button onClick={() => this.searchPlaces()}>검색하기</button>
                        </div>
                    </div>
                    <hr />
                    <ul id="placesList"></ul>
                    <div id="pagination"></div>
                </div>
            </>
        )
    }
}

export default KakaoMapExample

kakaomap_example.css

#map {
    width: 100vw;
    height: 100vh;
}

#menu_wrap {
    position: absolute;
    top: 0;
    left: 0;
    width: 250px;
    height: 700px;
    margin: 10px 0 30px 10px;
    padding: 5px;
    overflow-y: auto;
    background: rgba(255, 255, 255, 0.7);
    z-index: 2;
    font-size: 12px;
    border-radius: 10px;
}

.bg_white {
    background: #fff;
}

#menu_wrap hr {
    display: block;
    height: 1px;
    border: 0;
    border-top: 2px solid #5F5F5F;
    margin: 3px 0;
}

#menu_wrap .option {
    text-align: center;
}

#menu_wrap .option p {
    margin: 10px 0;
}

#menu_wrap .option button {
    margin-left: 5px;
}

#placesList li {
    list-style: none;
}

#placesList .item {
    position: relative;
    border-bottom: 1px solid #888;
    overflow: hidden;
    cursor: pointer;
    min-height: 65px;
}

#placesList .item span {
    display: block;
    margin-top: 4px;
}

#placesList .item h5,
#placesList .item .info {
    text-overflow: ellipsis;
    overflow: hidden;
    white-space: nowrap;
}

#placesList .item .info {
    padding: 10px 0 10px 55px;
}

#placesList .info .gray {
    color: #8a8a8a;
}

#placesList .info .jibun {
    padding-left: 26px;
    background: url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/places_jibun.png) no-repeat;
}

#placesList .info .tel {
    color: #009900;
}

#placesList .item .markerbg {
    float: left;
    position: absolute;
    width: 36px;
    height: 37px;
    margin: 10px 0 0 10px;
    background: url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/marker_number_blue.png) no-repeat;
}

#placesList .item .marker_1 {
    background-position: 0 -10px;
}

#placesList .item .marker_2 {
    background-position: 0 -56px;
}

#placesList .item .marker_3 {
    background-position: 0 -102px
}

#placesList .item .marker_4 {
    background-position: 0 -148px;
}

#placesList .item .marker_5 {
    background-position: 0 -194px;
}

#placesList .item .marker_6 {
    background-position: 0 -240px;
}

#placesList .item .marker_7 {
    background-position: 0 -286px;
}

#placesList .item .marker_8 {
    background-position: 0 -332px;
}

#placesList .item .marker_9 {
    background-position: 0 -378px;
}

#placesList .item .marker_10 {
    background-position: 0 -423px;
}

#placesList .item .marker_11 {
    background-position: 0 -470px;
}

#placesList .item .marker_12 {
    background-position: 0 -516px;
}

#placesList .item .marker_13 {
    background-position: 0 -562px;
}

#placesList .item .marker_14 {
    background-position: 0 -608px;
}

#placesList .item .marker_15 {
    background-position: 0 -654px;
}

#pagination {
    margin: 10px auto;
    text-align: center;
}

#pagination button {
    display: inline-block;
    background-color: transparent;
    border: none;
    margin-right: 10px;
    cursor: pointer;
}

#pagination button:hover{
    color: #777;
}

#pagination .on {
    font-weight: bold;
    cursor: pointer;
    background-color: transparent;
    color: #777;
}

< 리액트로 구현한 카카오맵 >

 

뭔가 굉장히 코드량이 많아보이지만 예제에 나와있는거 적당히(?) 짜집기 +@로 만들었습니다

 

먼저 바인딩하는 부분에서는 고민이 조금 필요했는데 여기저기 참고해서 필자가 적용한 부분은

 

componentDidMount부분에서 header에 script를 추가해줍니다

(autoload=false 필수)

 

script가 로드가 완료되면 카카오맵 예제에 나와있는 코드들을 적당히(?) react에 맞춰서 코딩을 해줍니다

 

추가로 vanila js에서는 spa와 달리 pagination부분에서 링크를 변경해주지만

 

필자는 rest api key를 이용해서 페이지에 따른 api콜을 다시 해주고 있습니다

 

displayPagination메소드 내부에서 pagination으로 페이지들을 생성할 때,

 

각각 버튼에 api콜을 다시해줍니다

const res = await fetch(`https://dapi.kakao.com/v2/local/search/keyword.json?page=${i}&size=15&sort=accuracy&query=${this.keyword}`, {
	headers: {
		'Authorization': `KakaoAK ${process.env.REACT_APP_KAKAO_RESTAPI_KEY}`
	}
})

page에 index값을 전달하고 keyword query부분에 keyword값을 넣어주고

 

header에 authorization에 api key를 셋팅해주고 콜을합니다

 

이후에 결과값을 displayPlaces 메소드에 넣어주면 마커들이 갱신됩니다

사실 kakao map webview 패키지도 동일한 방식으로 하면 될텐데 귀찮아서...

반응형
Comments