SPA(vuejs)에서 다음카카오맵 사용중입니다만 리소스를 해제하는 기능이 없네요.
map.destroy() 같은 기능이 없을까요?
페이지를 렌더링 할 때마다 지도가 중복으로 생성됩니다.
지도 API로 그리는 부분은
어떠한 장치 없이 Vue의 라이프 사이클을 그대로 따르면
부작용이 있을 수 밖에 없습니다.
map.destroy()를 찾으시기 보다는 (애초에 없기도 하지만요)
매 렌더링시 생성하는 코드에서
이미 생성되어 있으면 다시 생성하지 않는 형태로 변경하셔야 할 듯 합니다.
새로 페이지를 렌더링 할 때 기존 맵의 DOM Element가 body에서 제거되어있지만, 전역 스코프의 daum 객체가 맵 관련 리소스를 참조하고 있어 리소스가 해제되지 않는 것으로 보입니다. 이 때문에 다시 렌더링을 하지 않으면 지도가 보이지 않고, 다시 렌더링하면 지도가 두겹으로 생성됩니다.
이를 해결하기 위해서, 전역 스코프에 기존에 생성된 map을 기억해두고, 페이지를 새로 렌더링 할 때 새로이 생성되는 Element를 기존 map의 Element로 바꿔치기 할 수 있습니다.
이 외에도 다음 맵이 SPA 친화적인 라이브러리가 아니다보니 스크립트 비동기 로딩이나 변수 스코프의 문제 등 여러 tricky한 문제가 있었습니다. vuejs용 코드를 첨부하니 같은 문제를 겪는 분들은 참고해보실 수 있겠습니다.
맵 서비스 모듈
- 마커 클러스터, 마커 API 래핑
- 스크립트 동적으로 로드
- 렌더링시 기존 생성된 맵 Element 재사용
import loadScriptOnce from 'load-script-once'
const DAUM_KAKAO_API_JS_KEY = '===REDACTED==='
const DAUM_KAKAO_MAP_LIB_URL = `//dapi.kakao.com/v2/maps/sdk.js?appkey=${DAUM_KAKAO_API_JS_KEY}&libraries=drawing,clusterer,services&autoload=false`
class Map {
constructor () {
this.map = null
Map.initialize()
}
async mount (elementId) {
await Map.initialize()
// re-use map
if (Map.cachedMaps[elementId]) {
this.map = Map.cachedMaps[elementId]
const oldElement = this.map.getNode()
const newElement = document.getElementById(elementId)
newElement.parentNode.replaceChild(oldElement, newElement)
// create map
} else {
this.map = new Map.daum.maps.Map(
document.getElementById(elementId),
{
center: new Map.daum.maps.LatLng(35.65919598808177, 127.73894402702498),
level: 13,
},
)
this.map.setCopyrightPosition(Map.daum.maps.CopyrightPosition.BOTTOMRIGHT, true)
this.map.clusters = {}
this.map.markersWithoutCluster = []
Map.cachedMaps[elementId] = this.map
}
return this
}
addMarkerClusters (clusterSpecs = []) {
clusterSpecs.forEach(({ key, color, zIndex = 0, singleIconURL = null }) => {
if (this.map.clusters[key]) return
const cluster = this.map.clusters[key] = new Map.daum.maps.MarkerClusterer({
map: this.map,
averageCenter: true,
gridSize: 30,
minClusterSize: 2,
minLevel: 10,
disableClickZoom: false,
calculator: [10, 20], // 0~9, 10~19, 20~
styles: [
{
width: '30px',
height: '30px',
background: color,
opacity: 0.95,
border: '2px solid white',
borderRadius: '100%',
color: 'white',
textAlign: 'center',
lineHeight: '27px',
fontSize: '20px',
fontWeight: 'bold',
},
{
width: '36px',
height: '36px',
background: color,
opacity: 0.95,
border: '2px solid white',
borderRadius: '100%',
color: 'white',
textAlign: 'center',
lineHeight: '33px',
fontSize: '22px',
fontWeight: 'bold',
},
{
width: '48px',
height: '48px',
background: color,
opacity: 0.95,
border: '2px solid white',
borderRadius: '100%',
color: 'white',
textAlign: 'center',
lineHeight: '44px',
fontSize: '25px',
fontWeight: 'bold',
},
],
})
cluster._icon = new Map.daum.maps.MarkerImage(
singleIconURL,
new Map.daum.maps.Size(15, 15),
)
cluster._zIndex = zIndex
})
return this
}
addMarkers (markerSpecs = []) {
const markerSpecsWithoutClusterKey = []
const markerSpecsByClusterKey = markerSpecs.reduce((result, spec) => {
if (!spec.clusterKey) {
markerSpecsWithoutClusterKey.push(spec)
return result
}
if (!result[spec.clusterKey]) {
result[spec.clusterKey] = []
}
result[spec.clusterKey].push(spec)
return result
}, {})
markerSpecsWithoutClusterKey.forEach(({ lat, lng, title = null, onClick = null }) => {
const marker = new Map.daum.maps.Marker({
map: this.map,
position: new Map.daum.maps.LatLng(lat, lng),
title,
})
if (onClick) {
Map.daum.maps.event.addListener(marker, 'click', onClick)
}
this.map.markersWithoutCluster.push(marker)
})
for (let clusterKey in markerSpecsByClusterKey) {
const cluster = this.map.clusters[clusterKey]
cluster.addMarkers(
markerSpecsByClusterKey[clusterKey].map(({ lat, lng, title = null, onClick = null }) => {
const marker = new Map.daum.maps.Marker({
title,
position: new Map.daum.maps.LatLng(lat, lng),
image: cluster._icon,
zIndex: cluster._zIndex,
})
if (onClick) {
Map.daum.maps.event.addListener(marker, 'click', onClick)
}
return marker
})
)
}
}
clearMarkers () {
// remove cluster markers
for (let k in this.map.clusters) {
const cluster = this.map.clusters[k]
cluster.clear()
}
this.map.markersWithoutCluster.forEach(marker => {
marker.setMap(null)
})
this.map.markersWithoutCluster = []
}
setCenter ({ lat, lng, maxLevel = 8 }) {
if (this.map.getLevel() > maxLevel) {
this.map.setLevel(maxLevel)
}
this.map.panTo(
new Map.daum.maps.LatLng(lat, lng)
)
}
}
Map.cachedMaps = {}
Map.daum = null
Map.initialize = function () {
return new Promise((resolve, reject) => {
loadScriptOnce(DAUM_KAKAO_MAP_LIB_URL, (err) => {
if (err) return reject(err)
Map.daum = window.daum
Map.daum.maps.load(() => resolve())
})
})
}
export default Map
vue 컴포넌트
<template>
<div :id="elementId" :style="{ width, height }">
<!-- daum kakao map -->
</div>
</template>
<script>
import Map from '@/services/map/index'
// ...
export default {
props: {
elementId: {
type: String,
required: true,
},
markers: {
type: Array,
default () {
return []
},
required: true,
},
width: {
type: String,
required: false,
default: '100%',
},
height: {
type: String,
required: false,
default: '640px',
},
},
data () {
return {
map: null,
}
},
watch: {
markers: {
handler () {
if (typeof window === 'undefined') return // SSR
this.initMap(this.markers)
},
immediate: true,
},
},
methods: {
async initMap (markers) {
if (!this.map) {
const map = new Map()
await map.mount(this.elementId)
map.addMarkerClusters([
{
key: 'cluster1',
color: '#222529',
zIndex: 0,
singleIconURL: ClusterIcon1,
},
{
key: 'cluster2',
color: '#209cee',
zIndex: 1,
singleIconURL: ClusterIcon2,
},
])
this.map = map
} else {
this.map.clearMarkers()
}
this.map.addMarkers(
markers.map(
(marker) => {
const { name, type, location: { lat, lng } } = marker
return {
lat,
lng,
clusterKey: type,
title: name,
onClick: () => {
this.$emit('click-marker', marker)
},
}
}
)
)
},
setCenter (lat, lng) {
this.map && this.map.setCenter({ lat, lng, maxLevel: 10 })
},
},
}
</script>
4개의 좋아요
안녕하세요! 궁금한 사항이 있어서 질문 드립니다~!!
맵 서비스 모듈 - main.js 의 내용이고, vue 컴포넌트 - app.vue 의 내용이라고 생각하면 될까요??