[Vue.js] Vue Kakao 지도 구현 03 - Overlay 팝업 구현
Vue.js에서 Kakao 지도에 Overlay 팝업을 구현합니다.
[Vue.js] Vue Kakao 지도 구현 03 - Overlay 팝업 구현
프로젝트 구조
프로젝트구조[ROOT]
+- public
| +- index.html
+- src
+- components/map
| +- overlay
| +- index.js (+)
|
| +- KakaoMap.vue (E)
| +- marker-handler.js
|
+- service
| +- api.js
|
+- App.vue (E)
+- main.js
- overlay/index.js - Overlay를 담당하는 클래스
- App.vue - overaly에 사용될 element를 KakaoMap 안에 끼워넣음
- KakaoMap.vue - overlay용 element를 slot으로 정의함
Section1 에서는 전체 소스코드를 첨부합니다.
상세 설명을 보려면 Section 2 로 이동합니다.
1. Source Code
src/components/map/overlay/index.js (+)
Overlay 팝업을 담당하는 클래스(새로 추가)
.../map/overlay/index.jsconst kakao = window.kakao;
class KakaoOverlay {
constructor(vueMap, content) {
this.vueMap = vueMap;
this.content = content;
// init instance
this.instance = new kakao.maps.CustomOverlay({
map: null,
clickable: false,
content: content,
position: null,
xAnchor: 0.5,
yAnchor: 1,
zIndex: 3,
});
}
showAt(lat, lng) {
// (lat, lng )에 보여줌
console.log("overlay show!!", lat, lng);
this.instance.setMap(this.vueMap.mapInstance);
const pos = new kakao.maps.LatLng(lat, lng);
this.instance.setPosition(pos);
}
hide() {
this.instance.setMap(null);
}
}
export default KakaoOverlay;
src/App.vue
overlay에 사용될 element를 정의함
src/App.vue<template>
<div>
<h3>kakao map demo(center, level)</h3>
<div class="controll">
<button @click="zoom(-1)">
<span class="material-icons"> zoom_in </span>
</button>
<button @click="zoom(1)">
<span class="material-icons"> zoom_out </span>
</button>
</div>
<div class="map-area">
<div class="harbors">
<div
class="harbor"
v-for="hbr in harbors"
:key="hbr.seq"
@click="showOnMap(hbr)"
:class="{ active: hbr === activeHarbor }"
>
<h4>{{ hbr.place }}</h4>
</div>
</div>
<KakaoMap ref="kmap" class="kmap" :options="mapOption">
<div class="overlay-popup" ref="harborOverlay" slot="overlay">
<div v-if="overlayHarber">
<h3>{{ overlayHarber.place }}</h3>
<div class="addr">{{ overlayHarber.addr }}</div>
<a class="close" href="#" @click.prevent="closeOverlay()"
><span class="material-icons"> close </span></a
>
</div>
</div>
</KakaoMap>
</div>
</div>
</template>
<script>
import KakaoMap from "./components/map/KakaoMap.vue";
import api from "./service/api";
import MarkerHandler from "./components/map/marker-handler";
import KakaoOverlay from "./components/map/overlay";
export default {
components: {
KakaoMap,
},
data() {
return {
mapOption: {
center: {
lat: 33.450701,
lng: 126.570667,
},
level: 8,
},
harbors: [], // empty
markers: null, // marker handler!
activeHarbor: null, // selected harbor!
overlay: null, // overlay 인스턴스
overlayHarber: null, // overlay에 보여줄 항구
};
},
mounted() {
const vueKakaoMap = this.$refs.kmap;
this.markers = new MarkerHandler(vueKakaoMap, {
markerClicked: (harbor) => {
console.log("[clicked ]", harbor);
// this.activeHarbor = harbor;
this.showOnMap(harbor);
// 마커 클릭시
this.overlayHarber = harbor;
this.overlay.showAt(harbor.lat, harbor.lng);
},
});
this.overlay = new KakaoOverlay(vueKakaoMap, this.$refs.harborOverlay);
api.harbor.all((res) => {
console.log("[항구목록]", res.harbors);
this.harbors = res.harbors;
// create markers
this.markers.add(this.harbors, (harbor) => {
return { lat: harbor.lat, lng: harbor.lng };
});
});
},
methods: {
zoom(delta) {
// delta : 1 or -1
// console.log("[delta]", delta);
const level = Math.max(3, this.mapOption.level + delta); // min level 3
this.mapOption.level = level;
// console.log(this.mapOption.level);
},
showOnMap(harbor) {
this.activeHarbor = harbor;
// console.log("[center]", harbor);
this.mapOption.center = {
lat: harbor.lat,
lng: harbor.lng,
};
},
closeOverlay() {
this.overlay.hide();
},
},
};
</script>
<style lang="scss">
button {
border: 1px solid transparent;
padding: 6px;
background-color: #efefefdd;
border-radius: 6px;
&:hover {
background-color: #ddd;
border-color: #ddd;
cursor: pointer;
}
&:active {
background-color: #aaa;
border-color: #aaa;
}
}
.map-area {
display: flex;
.harbors {
.harbor {
padding: 10px;
border: 1px solid transparent;
&:hover {
background-color: aliceblue;
border-color: #6a9dff;
cursor: pointer;
}
&:active {
background-color: rgb(166, 197, 224);
border-color: #4471c5;
}
&.active {
background-color: rgb(253, 229, 150);
border-color: rgb(211, 173, 3);
}
h4 {
margin: 0;
}
}
}
.kmap {
flex: 1 1 auto;
.overlay-popup {
background-color: #ffffffcc;
box-shadow: 0 0 8px #0000004d, 0 0 1px 2px #00000022;
max-width: 200px;
min-width: 160px;
position: absolute;
bottom: 44px;
left: 50%;
transform: translateX(-50%);
h3 {
margin: 0;
padding: 8px;
padding-right: 24px;
background-color: #ed4215;
color: white;
font-weight: 400;
font-size: 16px;
}
.addr {
padding: 8px;
white-space: break-spaces;
}
.close {
color: black;
position: absolute;
top: 0;
left: 100%;
transform: translate(-50%, -50%);
background-color: white;
border-radius: 100%;
line-height: 0;
padding: 6px;
box-shadow: 0 0 6px #0000004d;
}
}
}
}
</style>
src/components/map/KakaoMap.vue
- named slot(<slot name="overlay"/>)을 정의
- 중심 좌표 이동시 setCenter 대신 panTo메소드 사용
KakaoMap.vue<template>
<div ref="map">
<slot name="overlay"></slot>
</div>
</template>
<script>
let kakao = window.kakao;
export default {
props: ["options"],
data() {
return {
mapInstance: null,
};
},
mounted() {
kakao = kakao || window.kakao;
console.log(this.$refs.map); // should be not null
var container = this.$refs.map;
const { center, level } = this.options;
this.mapInstance = new kakao.maps.Map(container, {
center: new kakao.maps.LatLng(center.lat, center.lng),
level,
});
},
watch: {
"options.level"(cur, prev) {
console.log(`[LEVEL CHANGED] ${prev} => ${cur}`); // for testing
this.mapInstance.setLevel(cur);
},
"options.center"(cur) {
this.mapInstance.panTo(new kakao.maps.LatLng(cur.lat, cur.lng));
// 부드럽게 이동
},
},
};
</script>
<style>
.kmap {
height: 600px;
}
</style>
2. 설명
Kakao 지도 api에서 제공하는 CustomOverlay를 vue.js 기반 프로젝트에서 구현할때 해결해야할 부분이 있습니다.
아래 그림은 overlay 사용 예제 페이지의 모습인데

대응하는 코드는 아래와 같습니다.
JAVASCRIPT// 커스텀 오버레이에 표출될 내용으로 HTML 문자열이나 document element가 가능합니다
var content = '<div class="customoverlay">' +
' <a href="https://map.kakao.com/link/map/11394059" target="_blank">' +
' <span class="title">구의야구공원</span>' +
' </a>' +
'</div>';
위처럼 문자열로 overlay html을 조립하는 식은 vue.js와 같은 spa 프로젝트에서는 지양해야할 부분입니다.
여기서는 vue.js 에서 제공하는 <slot/>
을 이용해서 overlay 화면을 렌더링하도록 합니다.
<slot /> 사용하기
overlay에서 사용할 html 을 KakaoMap
컴포넌트 안에 아래와 같이 끼워넣습니다.
App.vue<template>
<div>
....
<div class="map-area">
....
<KakaoMap ref="kmap" class="kmap" :options="mapOption">
<div class="overlay-popup" ref="harborOverlay" slot="overlay">
<div>
<h3>항구 이름 위치</h3>
<div class="addr">항구 주소 위치</div>
<a href="#">닫기</a>
</div>
</div>
</KakaoMap>
</div>
</div>
</template>
- 항구 정보를 보여주기 위해서 제목(
h3
)과 주소(.addr
) 를 준비함. - 닫기 버튼을 눌러 overlay를 닫는 기능도 추가함.
- ref - overlay 생성시 전달하기 위해서 ref로 element를 참조함
- slot="overlay" - KakaoMap 컴포넌트 안에 "overlay" 이름이 지정된 slot에 끼워넣겠음.
이제 KakaoMap.vue
안에서 대응하는 slot을 정의합니다.
KakaoMap.vue<template>
<div ref="map">
<slot name="overlay"></slot>
</div>
</template>
애플리케이션이 올바로 시작되면 slot 위치는 아래와 같이 App.vue에서 끼워넣었던 overlay의 element로 치환됩니다.
KakaoMap.vue<template>
<div ref="map">
<div class="overlay-popup">
<div>
<h3>항구 이름 위치</h3>
<div class="addr">항구 주소 위치</div>
<a href="#">닫기</a>
</div>
</div>
</div>
</template>
<div ref="map">
은 Kakao 지도 라이브러리가 지도 관련 이미지들을 끼워넣는 placeholder입니다. overlay의 element가 placehoder 의 자식 element로 존재하지만 overlay창을 띄울때 다른 곳으로 이동하게 됩니다.
overlay의 element가 다른 곳으로 이동해도 App.vue에서 닫기 버튼에 지정할 클릭 이벤트는 여전히 작동합니다.
KakaoOverlay 클래스 생성
공식 문서를 보면 overlay를 띄우는 코드는 아래와 같이 제공됩니다.
CustomOverlay 사용법var customOverlay = new kakao.maps.CustomOverlay({
map: ...,
content: ....,
position: new kakao.maps.LatLng(33.450701, 126.570667),
xAnchor: 0.5,
yAnchor: 1,
zIndex: 3
});
클래스 KakaoOverlay는 구체적인 overlay 생성 과정을 은닉하고, 부가 기능을 추가하는 곳입니다.
.../map/overlay/index.jsconst kakao = window.kakao;
class KakaoOverlay {
constructor(vueMap, content) {
this.vueMap = vueMap;
this.content = content;
// init instance
this.instance = new kakao.maps.CustomOverlay({
...
});
}
showAt(lat, lng) {
// 주어진 위치에 overlay를 렌더링함
}
hide() {
// 아래와 같이 null을 지정하면 overlay가 닫힘
this.instance.setMap(null);
}
}
export default KakaoOverlay;
- Vue 파일이 아닌 일반 js 파일
- vueMap -
KakaoMap.vue
파일의 인스턴스(VueComponent) - content - overlay의 화면 내용. 공식 문서에서는 대부분 html을 직접 조립하는 예제들을 보여주지만 이 위치에 dom element를 전달할 수도 있습니다. 여기서는
App.vue
에서 끼워넣었던 overlay element를 전달합니다.
KakaoOverlay 클래스는 App.vue
에서 아래와 같이 사용합니다.
App.vue<template>
</template>
<script>
...
// (1) 임포트
import KakaoOverlay from "./components/map/overlay";
export default {
data() {
return {
...
overlay: null, // (2) overlay 인스턴스 참조용
}
},
mounted() {
const vueKakaoMap = this.$refs.kmap;
this.markers = ....
// (3) 지도 컴포넌트와 overlay element를 전달
this.overlay = new KakaoOverlay(vueKakaoMap, this.$refs.harborOverlay);
....
},
}
</script>
- (1) KakaoOverlay 클래스를 import
- (2) overlay 인스턴스 참조할 변수
- (3) 지도 컴포넌트(KakaoMap.vue의 인스턴스)와 overlay의 content로 사용할 element 참조를 전달해서 생성자를 호출
이제 KakaoOverlay 클래스에서는 아래와 같이 실제 overlay를 준비합니다.
.../map/overlay/index.jsconst kakao = window.kakao;
class KakaoOverlay {
constructor(vueMap, content) {
this.vueMap = vueMap;
this.content = content;
// init instance
this.instance = new kakao.maps.CustomOverlay({
map: null,
clickable: false,
content: content,
xAnchor: 0.5,
yAnchor: 1,
zIndex: 3,
});
}
....
}
export default KakaoOverlay;
- Kakao 지도 라이브러리에서 제공하는 CustomOverlay 클래스를 사용할때 map property 에 null을 지정해서 아직 화면에 띄우지는 않음.
- xAnchor: 0.5 - 좌표가 overlay 화면의 좌우 너비의 가운데 위치하도록 함
- yAnchor: 1 - 좌표가 overlay 화면의 높이에서 야래에 위치하도록 함
- zIndex: 이 값을 조절해서 특정 overlay가 다른 overlay 위에 올라오도록 할 수도 있습니다.
실제로 좌표가 움직이지는 않습니다(움직이는건 overlay)
TXT(xAhcnor: 0.5, yAnchor: 1)
BBBBB
BBBBB
BBBBB
+
(xAhcnor: 0.5, yAnchor: 0)
+
BBBBB
BBBBB
BBBBB
(xAhcnor: 0, yAnchor: 0)
+
BBBBB
BBBBB
BBBBB
(xAhcnor: 0.5, yAnchor: 0.5)
BBBBB
BB+BB
BBBBB
마커 클릭
이제 App.vue에서 마커를 클릭할때 항구의 위치값을 전달해서 overlay를 띄웁니다.
App.vueexport default {
mounted() {
....
this.markers = new MarkerHandler(vueKakaoMap, {
markerClicked: (harbor) => {
....
this.showOnMap(harbor);
// 마커 클릭시 overlay를 보여줌
this.overlay.showAt(harbor.lat, harbor.lng);
},
});
this.overlay = ...
....
},
- showAt(lat, lng) 를 호출해서 overlay 를 띄움
KakaoOverlay 클래스에 showAt 메소드를 구현합니다.
.../map/overlay/index.jsconst kakao = window.kakao;
class KakaoOverlay {
constructor(vueMap, content) {
this.vueMap = vueMap;
this.content = content;
this.instance = ....
....
}
showAt(lat, lng) {
this.instance.setMap(this.vueMap.mapInstance);
const pos = new kakao.maps.LatLng(lat, lng);
this.instance.setPosition(pos);
}
}
- this.instance - kakao.mpas.CustomOverlay 로 생성했던 Overlay 객체
- this.vueMap.mapInstance - kakao.maps.Map 으로 생성했던 지도 객체
- setMap, setPosition 으로 위치 지정 후 지도에 렌더링함
Overly Design
App.vue에서 overlay를 아래와 같이 디자인 합니다.
App.vue<style lang="scss">
....
.map-area {
.kmap {
....
.overlay-popup {
background-color: #ffffffcc;
box-shadow: 0 0 8px #0000004d, 0 0 1px 2px #00000022;
max-width: 200px;
min-width: 160px;
h3 {
margin: 0;
padding: 8px;
padding-right: 24px;
background-color: #ed4215;
color: white;
font-weight: 400;
font-size: 16px;
}
.addr {
padding: 8px;
white-space: break-spaces;
}
}
}
}
</style>
마커를 클릭하면 아래와 같이 좌표 위에 overlay가 나타납니다.

overlay는 원하는 위치에 렌더링 되었으나 마커와 겹쳐서 보기에 좋지 않습니다.
.overlay-popup
에 위치를 수정하는 스타일을 추가합니다.
App.vue<style lang="scss">
....
.map-area {
.kmap {
....
.overlay-popup {
....
position: absolute;
bottom: 44px;
left: 50%;
transform: translateX(-50%);
h3 {
...
}
.addr {
...
}
}
}
}
</style>
- bottom: 44px - 기본 마커의 높이만큼 간격을 줘서 겹치지 않게 함
- left, transform - 가운데 정렬
close 버튼 디자인은 동영상 참조 https://youtu.be/2wEKmcdVWmA
Overlay 내용 채우기
마커를 클릭하면 overlay에 항구 정보를 보여주고 싶습니다.
overlay에 보여줄 항구를 나타낼 변수를 정의하고 overay element에 항구이름과 주소를 바인딩합니다.
App.vue<template>
....
<KakaoMap ...>
<div class="overlay-popup" ...>
<div v-if="overlayHarber"><!-- (3) 렌더링 -->
<h3>{{ overlayHarber.place }}</h3>
<div class="addr">{{ overlayHarber.addr }}</div>
<a class="close" href="#" @click.prevent="closeOverlay()"
><span class="material-icons"> close </span></a
>
</div>
</div>
</KakaoMap>
</template>
<script>
export default {
data() {
return {
...
overlay: null,
overlayHarber: null, // (1) overlay에 보여줄 항구
}
},
mounted() {
....
this.markers = new MarkerHandler(vueKakaoMap, {
markerClicked: (harbor) => {
...
// 마커 클릭시
this.overlayHarber = harbor; // (2) 클릭한 항구 바인딩
this.overlay.showAt(harbor.lat, harbor.lng);
},
});
....
},
methods: {
...
closeOverlay() {
this.overlay.hide(); // (4) 오버레이 닫음
}
}
}
</script>
- (1, 2) 마커 클릭시 대응하는 항구를 변수에 바인딩함 (this.overlayHarber = harbor)
- (3) 템플릿에서
overlayHarbor
를 overlay element에 렌더링함 - (4) close 버튼 클릭시 overlay를 닫음
마지막으로 KakaoOverlay
클래스에 메소드 hide()를 구현합니다.
.../map/overlay/index.jsconst kakao = window.kakao;
class KakaoOverlay {
...
showAt(lat, lng) { ... }
hide() {
this.instance.setMap(null);
}
}
- overlay를 닫고 싶으면 지도객체를 null로 지정함 - CustomOverlay.setMap(null)