Spa에서 다음 카카오 지도 사용중입니다만 리소스를 해제하는 기능이 없네요

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 의 내용이라고 생각하면 될까요??