본문 바로가기
Web

[Bootstrap] 카카오맵 활용한 홈페이지 만들기 (1)

by DuncanKim 2022. 8. 11.
728x90

[Bootstrap] 카카오맵 활용한 홈페이지 만들기 (1)

 

 

1. 프론트 삽질

 

첫 번째 귀여운 프로젝트를 진행 중이다. 현재 나는 카카오맵 API를 가지고 여러 기능들을 추가하고, 하나의 홈페이지로 구현을 해보는 것을 진행하고 있다. 현재 백엔드 과정을 진행 중이지만, html, css, js, jquery만 며칠 동안 봐서 어질어질하다. 그렇지만, 기본적인 것은 할 줄 알면 좋은 법. 익혀나가고 있다.

 

그냥 템플릿을 사용하지 않고 만들다가 정말 모니터를 부술뻔 했다. 컴퓨터는 아무 잘못이 없지만, 내가 준비한 카카오맵 API 구현 코드와 부트스트랩을 적용한 UI가 충돌하고, 내가 준비한 카카오맵 코드도 기능을 추가하다 보니 엉켜서 엉망진창이 되어 버렸다.

 

구현한 코드 중 일부는 아래와 같다. 여기에서 문제가 많이 발생했다. 어떤 템플릿의 일부 코드만 가져와서 그것을 활용했기 때문에 카카오맵 API 등의 많은 기능들이 충돌을 일으킨 것이다.

   <!-- 왼쪽 네비게이션 바 -->
    <div class="container-fluid">
        <div class="row flex-nowrap">
            <div class="col-lg col-md-3 col-xl-2 px-sm-auto px-0 " style="background-color: #e3f2fd ">
                <div class="d-flex flex-column align-items-center align-items-sm-start px-3 pt-2 text-white min-vh-100">
                    <a href="/" class="d-flex align-items-center pb-3 mb-md-0 me-md-auto text-dark text-decoration-none">
                        <span class="fs-5 d-none d-sm-inline">메뉴</span>
                    </a>
                    <ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
                        <li class="nav-item">
                            <a href="#" class="nav-link align-middle px-0 text-dark">
                                <span class="ms-1 d-none d-sm-inline">회원정보관리</span>
                            </a>
                        </li>
                        <li>
                            <a href="#submenu1" class="nav-link px-0 align-middle text-dark">
                                <span class="ms-1 d-none d-sm-inline">Dashboard</span>
                            </a>
                        </li>
                        <li>
                            <a href="#" class="nav-link px-0 align-middle text-dark">
                                <span class="ms-1 d-none d-sm-inline">기능3</span></a>
                        </li>
                        <li>
                            <a href="#submenu2" data-bs-toggle="collapse" class="nav-link px-0 align-middle text-dark">
                                <span class="ms-1 d-none d-sm-inline">토글박스</span></a>
                            <ul class="collapse nav flex-column ms-1" id="submenu2" data-bs-parent="#menu">
                                <li class="w-100">
                                    <a href="#" class="nav-link px-0"> <span class="d-none d-sm-inline">Item</span> 1</a>
                                </li>
                                <li>
                                    <a href="#" class="nav-link px-0"> <span class="d-none d-sm-inline">Item</span> 2</a>
                                </li>
                            </ul>
                        </li>
                    </ul>
                    <hr>
                    <!-- 소스 : https://dev.to/codeply/bootstrap-5-sidebar-examples-38pb -->
                    <div class="btn-group dropup pb-4">
                        <a href="#" class="d-flex align-items-center text-dark text-decoration-none dropdown-toggle" id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
                            <img src="https://github.com/mdo.png" alt="hugenerd" width="40" height="40" class="rounded-circle">
                            <span class="d-none d-sm-inline mx-1">이용자 별명</span>
                        </a>
                        <ul class="dropdown-menu dropdown-menu-dark text-small shadow" aria-labelledby="dropdownUser1">
                            <li><a class="dropdown-item" href="#">내정보관리</a></li>
                            <li><a class="dropdown-item" href="#">절약한 배달비 보기</a></li>
                            <li><hr class="dropdown-divider"></li>
                            <li><a class="dropdown-item" href="#">로그아웃</a></li>
                        </ul>
                    </div>
                </div>
            </div>

            <!-- map -->
            <div class="map_wrap col py-3">
                <div class="col-lg col-md-3 col-xl-2 px-sm-auto px-0 min-vh-100" id="map" style="margin:0; width: 70%; height: 70%;"></div>
                <ul id="category">
                    <li id="BK9" data-order="0">
                        <span class="category_bg bank"></span>
                        한식
                    </li>
                    <li id="MT1" data-order="1">
                        <span class="category_bg mart"></span>
                        중식
                    </li>
                    <li id="PM9" data-order="2">
                        <span class="category_bg pharmacy"></span>
                        일식
                    </li>
                    <li id="OL7" data-order="3">
                        <span class="category_bg oil"></span>
                        양식
                    </li>
                    <li id="CE7" data-order="4">
                        <span class="category_bg cafe"></span>
                        분식
                    </li>
                    <li id="CS2" data-order="5">
                        <span class="category_bg store"></span>
                        야식
                    </li>
                </ul>
            </div>
        </div>
    </div>

<script type="text/javascript" src="https://dapi.kakao.com/v2/maps/sdk.js?appkey=앱키&libraries=services"></script>
<script>
    // 1. 지도 생성

    // 초기 지도 센터값 설정
    var x = 37.48875;
    var y = 126.93687;

    // 주소-좌표 변환 객체를 생성합니다
    var mapContainer = document.getElementById('map'), // 지도를 표시할 div

    mapOption = {
        center: new kakao.maps.LatLng(x, y), // 지도의 중심좌표
        level: 3, // 지도의 확대 레벨
        mapTypeId : kakao.maps.MapTypeId.ROADMAP // 지도종류
    };

    // 지도를 생성한다
    var map = new kakao.maps.Map(mapContainer, mapOption);
    var geocoder = new kakao.maps.services.Geocoder();

    // 확대, 축소 기능(줌 컨트롤)
    var zoomControl = new kakao.maps.ZoomControl();
    map.addControl(zoomControl, kakao.maps.ControlPosition.BOTTOMRIGHT); // 파라미터(컨트롤, 위치)
    kakao.maps.event.addListener(map, "zoom_changed', function() {
        level = map.getLevel();
    }
    // gps 위치 버튼(직접 구현 필요. 마커가 이동하는 것 처럼만 보이게 하면 될듯.)

 

 

결과적으로는 이런 뒤죽박축 화면이 탄생하게 되었고, 하나씩 기능하던 동작조차 제기능을 하지 못하는 상황이 되었다.

 

 

..... 영역에 대한 개념도 없고 그냥 막 갖다 쓰면서 무엇인가 괴물이 만들어져 버렸다. 여기에 모달창까지 띄워보려 했으나, 클릭조차 되지 않는 모습을 보여주었다. 알아보니, 모달창이 카카오맵과 함께 쓰일 때, 특정 css에서 충돌이 일어날 수도 있다는 사실을 알게 되었다.

 

https://devtalk.kakao.com/t/bootstrap/111302

 

Bootstrap 적용 후 카카오 지도의 컨트롤 바의 디자인 및 기능 문제 발생

bootstrap의 navbar와 함께 지도와 컨트롤 바를 화면 상에 띄우고자 하였는데 컨트롤 바가 제대로 보이지 않아요. 여러 가지를 시도하여 보니 base html로 사용하였던 부분의 부분을 삭제하면 제대로

devtalk.kakao.com

 

일단 뭐든 갖다 쓴다는 마음을 가졌는데, 교통정리를 잘 해놓으면서 코드를 갖다 써야 한다는 것을 알 수 있었다. 꼬여버린 것은 다시 복구하기는 코드를 쓴 나도 어렵다. 주석을 달아놨다 해도, 추가된 것들이 많기 때문에 이 변수, 저 메서드가 왜 존재하는지 가물가물 한 것들이 많기 때문이다. 그래서 다시 새로운 템플릿을 가져와서 진행해보기로 했다.

 

 

2. startbootstrap을 만나다.

 

https://startbootstrap.com/

 

Free Bootstrap Themes, Templates, Snippets, and Guides - Start Bootstrap

Landing Page A clean, functional landing page theme

startbootstrap.com

 

부트스트랩 무료 템플릿을 찾아보다가 우리 컨셉과 맞는 템플릿을 찾았다. 창이동이 별로 없고, 네비게이션 바에 그다지 많은 항목이 필요하지 않았고, 메인 화면에는 지도만 띄워지고, 그 안에서 마커가 띄워지는 등의 액션이 존재하기 때문에 최대한 군더더기가 없는 것을 골라보았다. 그 결과 바로 이 친구를 만날 수 있었다.

 

SB-Admin이라는 템플릿으로 로그인 기능, 회원가입 기능을 추가할 수 있는 템플릿이다. 메인 index.html 파일 안에는 여기에서 구현할 수 있는 차트, 버튼 등을 소개해주고 있다. 간단하기도 하고, 일단 html, css 등 폴더, 파일 구성이 간단했기 때문에 이것을 활용해서 구현해보기로 하였다. (React는 모르기에...)

 

 

3. 카카오맵 API와 템플릿 결합시키기

 

카카오맵 API는 html과 js로 되어있다. 카카오 디벨로퍼 홈페이지에서 안드로이드, IOS, 웹 용 API 적용 코드를 볼 수 있다. 그 코드의 적용방법이나, 이것들을 활용해서 내가 원하는 기능으로 구현하는 것은 다음에 포스팅을 할 예정이다.

 

어쨌든, 처음에 템플릿을 다운로드하면 이런 html 문서가 여러 개 있는 것을 볼 수 있다. 스스로 바꿔서 원하는 대로 바꿀 수 있으며, 배치도 마음대로 바꿀 수 있다. 

 

 

여기에서 나는 content 항목에 지도를 붙여 넣었다. 이 템플릿의 경우, homepage content라는 주석 부분이 있는데, 이 부분에 내가 원하는 컨텐츠를 넣어주면 된다. 나 같은 경우에는 지도가 될 것이다.

 

<카카오맵 지도 api html 코드>

    <!--homepage content-->
    <div id="layoutSidenav_content">

        <main>
            <!--map 구현!-->
            <div class="map_wrap">
                <div id="map" style="width:100%;height:100%;position:relative;overflow:hidden;"></div>
                <ul id="category">
                    <li id="BK9" data-order="0">
                        <span class="category_bg bank"></span>
                        은행
                    </li>
                    <li id="MT1" data-order="1">
                        <span class="category_bg mart"></span>
                        마트
                    </li>
                    <li id="PM9" data-order="2">
                        <span class="category_bg pharmacy"></span>
                        약국
                    </li>
                    <li id="OL7" data-order="3">
                        <span class="category_bg oil"></span>
                        주유소
                    </li>
                    <li id="CE7" data-order="4">
                        <span class="category_bg cafe"></span>
                        카페
                    </li>
                    <li id="CS2" data-order="5">
                        <span class="category_bg store"></span>
                        편의점
                    </li>
                </ul>
            </div>

            <script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=1f05502ebf3d151d4c3a36b309d1b05e&libraries=services"></script>
            <script>

 

이렇게 맵을 간단하게 넣은 다음에, map_wrap의 css 속성을 바꿔주면 크기 조절이 가능하다. 크롬의 경우 개발자 도구를 활용해서 요소의 태그가 무엇인지 확인해보면서 해보는 것도 좋은 방법이다.

 


cf. 개발자도구 사용해서 태그 알아내는 방법(크롬)

 

1) 오른쪽 위의 점 세 개 클릭 - 도구 더보기 - 개발자 도구

 

2) 개발자 도구 왼쪽 위에 화살표, 창으로 되어 있는 것을 클릭한다. 파란색으로 되면 된다.

 

 

3) 페이지에 마우스 커서를 가져다 대보면, 커서가 가리키는 곳의 태그명 또는 id, class 명과 현재 설정되어 있는 속성 값을 볼 수 있다.


 

<카카오맵 자바스크립트, html style 태그 안 속성>

 

아래의 코드는 너무 기니까, 적용한 코드만을 설명하자면, 

 

https://apis.map.kakao.com/web/sample/ 사이트에서 지도 생성하기, 카테고리별 장소 검색하기(라이브러리 사용)만 적용을 하였다. 이렇게 적용을 하면, localhost:8080으로 들어갔을 때, 사이트에 지도가 떠있는 것을 볼 수 있을 것이다.

 

처음 보는 사람들은 너무 길고, 또 방대하게 많아서 하기 싫어지겠지만, 어떤 태그가 어떤 기능을 하는지 삽질하는 시간을 가지다 보면, 무엇을 바꿔도 되고, 무엇을 바꾸면 안 되는지를 알게 된다. 하나만 이야기하자면, 카카오 API가 제공하는 메서드는 변경하지 못한다는 점을 명심해야 한다. 예를 들어 여기에는 없지만, LatLng 클래스에 getLng()이라는 메서드가 있다. 이 경우 우리는, 카카오에서 제공하는 기능만 사용할 수 있고 그 메서드를 오버라이딩 하거나, 수정해서 사용할 수가 없다. 당연할 법한 내용이지만, 처음 API를 사용해본 나는 이것 조차 고치려고 하여 오류를 몇 번 낸 적이 있었다....

 

<!--map css -->
<style>

.map #centerRohAddr {display:block;margin-top:2px;font-weight: normal;}
.map #centerDongAddr {display:block;margin-top:2px;font-weight: normal;}
.map .bAddr {padding:5px;text-overflow: ellipsis;overflow: hidden;white-space: nowrap;}
.map_wrap, .map_wrap * {margin:0; padding:0;font-family:'Malgun Gothic',dotum,'돋움',sans-serif;font-size:12px;}
.map_wrap {z-index: 0; position:absolute; width:100%;height:100%;}
#category {position:absolute;top:10px;left:10px;border-radius: 5px; border:1px solid #909090;box-shadow: 0 1px 1px rgba(0, 0, 0, 0.4);background: #fff;overflow: hidden;z-index: 2;}
#category li {float:left;list-style: none;width:50px;px;border-right:1px solid #acacac;padding:6px 0;text-align: center; cursor: pointer;}
#category li.on {background: #eee;}
#category li:hover {background: #ffe6e6;border-left:1px solid #acacac;margin-left: -1px;}
#category li:last-child{margin-right:0;border-right:0;}
#category li span {display: block;margin:0 auto 3px;width:27px;height: 28px;}
#category li .category_bg {background:url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/places_category.png) no-repeat;}
#category li .bank {background-position: -10px 0;}
#category li .mart {background-position: -10px -36px;}
#category li .pharmacy {background-position: -10px -72px;}
#category li .oil {background-position: -10px -108px;}
#category li .cafe {background-position: -10px -144px;}
#category li .store {background-position: -10px -180px;}
#category li.on .category_bg {background-position-x:-46px;}
.placeinfo_wrap {position:absolute;bottom:28px;left:-150px;width:300px;}
.placeinfo {position:relative;width:100%;border-radius:6px;border: 1px solid #ccc;border-bottom:2px solid #ddd;padding-bottom: 10px;background: #fff;}
.placeinfo:nth-of-type(n) {border:0; box-shadow:0px 1px 2px #888;}
.placeinfo_wrap .after {content:'';position:relative;margin-left:-12px;left:50%;width:22px;height:12px;background:url('https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/vertex_white.png')}
.placeinfo a, .placeinfo a:hover, .placeinfo a:active{color:#fff;text-decoration: none;}
.placeinfo a, .placeinfo span {display: block;text-overflow: ellipsis;overflow: hidden;white-space: nowrap;}
.placeinfo span {margin:5px 5px 0 5px;cursor: default;font-size:13px;}
.placeinfo .title {font-weight: bold; font-size:14px;border-radius: 6px 6px 0 0;margin: -1px -1px 0 -1px;padding:10px; color: #fff;background: #d95050;background: #d95050 url(https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/arrow_white.png) no-repeat right 14px center;}
.placeinfo .tel {color:#0f7833;}
.placeinfo .jibun {color:#999;font-size:11px;margin-top:0;}
</style>
<!--map css 끝!-->

<script type="text/javascript" src="//dapi.kakao.com/v2/maps/sdk.js?appkey=개인앱키&libraries=services"></script>
<script>

// 마커를 클릭했을 때 해당 장소의 상세정보를 보여줄 커스텀오버레이입니다
var placeOverlay = new kakao.maps.CustomOverlay({zIndex:1}),
    contentNode = document.createElement('div'), // 커스텀 오버레이의 컨텐츠 엘리먼트 입니다
    markers = [], // 마커를 담을 배열입니다
    currCategory = ''; // 현재 선택된 카테고리를 가지고 있을 변수입니다

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

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

// 장소 검색 객체를 생성합니다
var ps = new kakao.maps.services.Places(map);

// 지도에 idle 이벤트를 등록합니다
kakao.maps.event.addListener(map, 'idle', searchPlaces);

// 커스텀 오버레이의 컨텐츠 노드에 css class를 추가합니다
contentNode.className = 'placeinfo_wrap';

// 커스텀 오버레이의 컨텐츠 노드에 mousedown, touchstart 이벤트가 발생했을때
// 지도 객체에 이벤트가 전달되지 않도록 이벤트 핸들러로 kakao.maps.event.preventMap 메소드를 등록합니다
addEventHandle(contentNode, 'mousedown', kakao.maps.event.preventMap);
addEventHandle(contentNode, 'touchstart', kakao.maps.event.preventMap);

// 커스텀 오버레이 컨텐츠를 설정합니다
placeOverlay.setContent(contentNode);

// 각 카테고리에 클릭 이벤트를 등록합니다
addCategoryClickEvent();

// 엘리먼트에 이벤트 핸들러를 등록하는 함수입니다
function addEventHandle(target, type, callback) {
    if (target.addEventListener) {
        target.addEventListener(type, callback);
    } else {
        target.attachEvent('on' + type, callback);
    }
}

// 카테고리 검색을 요청하는 함수입니다
function searchPlaces() {
    if (!currCategory) {
        return;
    }

    // 커스텀 오버레이를 숨깁니다
    placeOverlay.setMap(null);

    // 지도에 표시되고 있는 마커를 제거합니다
    removeMarker();

    ps.categorySearch(currCategory, placesSearchCB, {useMapBounds:true});
}

// 장소검색이 완료됐을 때 호출되는 콜백함수 입니다
function placesSearchCB(data, status, pagination) {
    if (status === kakao.maps.services.Status.OK) {

        // 정상적으로 검색이 완료됐으면 지도에 마커를 표출합니다
        displayPlaces(data);
    } else if (status === kakao.maps.services.Status.ZERO_RESULT) {
        // 검색결과가 없는경우 해야할 처리가 있다면 이곳에 작성해 주세요

    } else if (status === kakao.maps.services.Status.ERROR) {
        // 에러로 인해 검색결과가 나오지 않은 경우 해야할 처리가 있다면 이곳에 작성해 주세요

    }
}

// 지도에 마커를 표출하는 함수입니다
function displayPlaces(places) {

    // 몇번째 카테고리가 선택되어 있는지 얻어옵니다
    // 이 순서는 스프라이트 이미지에서의 위치를 계산하는데 사용됩니다
    var order = document.getElementById(currCategory).getAttribute('data-order');

    for ( var i=0; i<places.length; i++ ) {

            // 마커를 생성하고 지도에 표시합니다
            var marker = addMarker(new kakao.maps.LatLng(places[i].y, places[i].x), order);

            // 마커와 검색결과 항목을 클릭 했을 때
            // 장소정보를 표출하도록 클릭 이벤트를 등록합니다
            (function(marker, place) {
                kakao.maps.event.addListener(marker, 'click', function() {
                    displayPlaceInfo(place);
                });
            })(marker, places[i]);
    }
}

// 마커를 생성하고 지도 위에 마커를 표시하는 함수입니다
function addMarker(position, order) {
    var imageSrc = 'https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/places_category.png', // 마커 이미지 url, 스프라이트 이미지를 씁니다
        imageSize = new kakao.maps.Size(27, 28),  // 마커 이미지의 크기
        imgOptions =  {
            spriteSize : new kakao.maps.Size(72, 208), // 스프라이트 이미지의 크기
            spriteOrigin : new kakao.maps.Point(46, (order*36)), // 스프라이트 이미지 중 사용할 영역의 좌상단 좌표
            offset: new kakao.maps.Point(11, 28) // 마커 좌표에 일치시킬 이미지 내에서의 좌표
        },
        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;
}

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

// 클릭한 마커에 대한 장소 상세정보를 커스텀 오버레이로 표시하는 함수입니다
function displayPlaceInfo (place) {
    var content = '<div class="placeinfo">' +
                    '   <a class="title" href="' + place.place_url + '" target="_blank" title="' + place.place_name + '">' + place.place_name + '</a>';

    if (place.road_address_name) {
        content += '    <span title="' + place.road_address_name + '">' + place.road_address_name + '</span>' +
                    '  <span class="jibun" title="' + place.address_name + '">(지번 : ' + place.address_name + ')</span>';
    }  else {
        content += '    <span title="' + place.address_name + '">' + place.address_name + '</span>';
    }

    content += '    <span class="tel">' + place.phone + '</span>' +
                '</div>' +
                '<div class="after"></div>';

    contentNode.innerHTML = content;
    placeOverlay.setPosition(new kakao.maps.LatLng(place.y, place.x));
    placeOverlay.setMap(map);
}


// 각 카테고리에 클릭 이벤트를 등록합니다
function addCategoryClickEvent() {
    var category = document.getElementById('category'),
        children = category.children;

    for (var i=0; i<children.length; i++) {
        children[i].onclick = onClickCategory;
    }
}

// 카테고리를 클릭했을 때 호출되는 함수입니다
function onClickCategory() {
    var id = this.id,
        className = this.className;

    placeOverlay.setMap(null);

    if (className === 'on') {
        currCategory = '';
        changeCategoryClass();
        removeMarker();
    } else {
        currCategory = id;
        changeCategoryClass(this);
        searchPlaces();
    }
}

// 클릭된 카테고리에만 클릭된 스타일을 적용하는 함수입니다
function changeCategoryClass(el) {
    var category = document.getElementById('category'),
        children = category.children,
        i;

    for ( i=0; i<children.length; i++ ) {
        children[i].className = '';
    }

    if (el) {
        el.className = 'on';
    }
}
</script>

 

아무튼 하나의 html 문서 안에 이 코드와 위의 html 태그들을 정렬해서 배치하여 localhost:8080으로 돌려보면 사진과 같은 결과물을 얻을 수 있다.

 

 

아직 미완의 단계이지만, 조금 더 진행을 해서 2편에서는 API 기능을 소개해보려고 한다.

728x90

댓글