개요
이 튜토리얼에서는 마커 클러스터를 사용하여 지도에 많은 수의 마커를 표시하는 방법을 설명합니다. @googlemaps/markerclusterer 라이브러리를 Maps JavaScript API와 함께 사용하면 가까이 있는 마커를 클러스터로 결합하고 지도상에 마커를 간단하게 표시할 수 있습니다.
마커 클러스터링의 작동 방식을 보려면 다음 지도를 참고하세요.
클러스터의 숫자는 클러스터에 포함된 마커의 수를 나타냅니다. 클러스터 위치를 확대하면 클러스터의 숫자가 감소하고 지도에 개별 마커가 표시되기 시작합니다. 지도를 축소하면 마커가 클러스터로 다시 통합됩니다.
다음 샘플은 이 지도를 만드는 데 필요한 전체 코드를 보여줍니다.
TypeScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); const mapElement = document.querySelector<google.maps.MapElement>('gmp-map')!; const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); // markers can only be keyboard focusable when they have click listeners // open info window when marker is clicked marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
JavaScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); const mapElement = document.querySelector('gmp-map'); const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); // markers can only be keyboard focusable when they have click listeners // open info window when marker is clicked marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
CSS
html, body { height: 100%; margin: 0; padding: 0; }
HTML
<html>
<head>
<title>Marker Clustering</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "GOOGLE_MAPS_API_KEY",
});
</script>
</head>
<body>
<gmp-map
center="-28.024,140.887"
zoom="3"
map-id="DEMO_MAP_ID"></gmp-map>
</body>
</html>편의상 이 튜토리얼에서는 locations 배열을 사용하여 지도에 마커 집합을 추가합니다. 실제로는 다른 소스도 사용 가능합니다.
자세한 내용은 마커 만들기 가이드를 참고하세요.
마커 클러스터 추가
마커 클러스터러를 추가하려면 다음 단계를 따르세요.
- 페이지 또는 애플리케이션에 마커 클러스터링 라이브러리를 추가합니다. 라이브러리는 NPM(
@googlemaps/Markerclusterer
) 및
GitHub 저장소에 제공됩니다.
다음 옵션 중 하나를 선택하여 라이브러리를 설치합니다.
NPM
NPM을 사용하여 @googlemaps/markerclusterer 라이브러리를 설치합니다.
npm install @googlemaps/markerclusterer
CDN
다음 스크립트는 unpkg.com CDN에서 라이브러리를 로드합니다.
<script src="https://unpkg.com/@googlemaps/markerclusterer/dist/index.min.js"></script>
- 앱에 마커 클러스터러를 추가합니다.
다음 코드는 지도에 마커 클러스터러를 추가합니다. 다음 옵션 중 하나를 선택합니다.
NPM
import { MarkerClusterer } from "@googlemaps/markerclusterer"; const markerCluster = new MarkerClusterer({ markers, map });
CDN
CDN을 사용하여 액세스하는 경우
markerClusterer전역에서 라이브러리를 사용할 수 있습니다.const markerCluster = new markerClusterer.MarkerClusterer({ markers, map });
이 샘플은
markers배열을MarkerClusterer에 전달합니다. - (선택사항) 마커 클러스터러를 맞춤설정하여 클러스터 아이콘 모양, 클러스터링 알고리즘 또는 클릭 동작을 변경합니다.
마커 클러스터러 맞춤설정
클러스터러 인스턴스를 만들 때
MarkerClustererOptions
객체에 추가 옵션을 전달하여 MarkerClusterer를 맞춤설정할 수 있습니다.
renderer: 지도에 클러스터 마커가 렌더링되는 방식을 제어합니다.algorithm및algorithmOptions: 거리 및 확대/축소 수준에 따라 마커가 클러스터로 그룹화되는 방식을 제어합니다.onClusterClick: 사용자가 클러스터 마커를 클릭할 때 기본 동작을 재정의합니다.
다음 지도에는 맞춤 클러스터 아이콘이 있는 마커가 표시됩니다.
클러스터 아이콘 맞춤설정
기본적으로 MarkerClusterer는
DefaultRenderer
을 사용하여 SVG 클러스터 아이콘을 만듭니다. 클러스터 마커의 모양을 맞춤설정하려면
Renderer
인터페이스를 구현하는 객체를 renderer 옵션에 전달합니다.
Renderer 인터페이스에는 AdvancedMarkerElement (또는 기존 google.maps.Marker)를 반환하는 render(cluster, stats, map) 메서드가 필요합니다. render 메서드는 다음 인수를 받습니다.
cluster(Cluster): 다음을 포함한 마커 그룹을 나타냅니다:count: 클러스터에 표시되는 마커 수입니다.position: 클러스터의google.maps.LatLng중심 위치입니다.bounds: 클러스터의google.maps.LatLngBounds입니다.markers: 클러스터에 포함된 마커 배열입니다.
stats(ClusterStats): 현재 렌더링 주기의 모든 클러스터에 대한 통계를 제공합니다. 예:stats.clusters.markers.mean,min,max,sum. 이러한 값을 사용하여 상대적 크기에 따라 클러스터의 스타일을 동적으로 지정할 수 있습니다.map(google.maps.Map): 지도 인스턴스입니다.
다음 예에서는 각
AdvancedMarkerElement에 원형 HTML 배지를 만들고 클러스터 마커 수가 평균 클러스터 크기를 초과할 때 배경 색상을 변경하는 맞춤 renderer를 정의합니다.
TypeScript
// Customize the cluster icons using a custom Renderer const renderer = { render: ( { count, position, }: { count: number; position: google.maps.LatLng }, stats: ClusterStats ) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); }
JavaScript
// Customize the cluster icons using a custom Renderer const renderer = { render: ({ count, position }, stats) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); }
전체 맞춤 렌더러 예시 보기
TypeScript
import { MarkerClusterer, ClusterStats } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); await customElements.whenDefined('gmp-map'); const mapElement = document.querySelector<google.maps.MapElement>('gmp-map')!; const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Customize the cluster icons using a custom Renderer const renderer = { render: ( { count, position, }: { count: number; position: google.maps.LatLng }, stats: ClusterStats ) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
JavaScript
import { MarkerClusterer } from '@googlemaps/markerclusterer'; async function init() { // Request needed libraries. const [{ InfoWindow }, { AdvancedMarkerElement, PinElement }] = await Promise.all([ google.maps.importLibrary('maps'), google.maps.importLibrary('marker'), ]); await customElements.whenDefined('gmp-map'); const mapElement = document.querySelector('gmp-map'); const innerMap = mapElement.innerMap; const infoWindow = new InfoWindow({ content: '', disableAutoPan: true, }); // Create an array of alphabetical characters used to label the markers. const labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; // Add some markers to the map. const markers = locations.map((position, i) => { const label = labels[i % labels.length]; const pinGlyph = new PinElement({ glyphText: label, glyphColor: 'white', }); const marker = new AdvancedMarkerElement({ position, content: pinGlyph, gmpClickable: true, }); marker.addEventListener('gmp-click', () => { const content = document.createElement('div'); content.textContent = `${String(position.lat)}, ${String(position.lng)}`; infoWindow.setContent(content); infoWindow.open({ anchor: marker, map: innerMap, }); }); return marker; }); // Customize the cluster icons using a custom Renderer const renderer = { render: ({ count, position }, stats) => { // Change color based on the number of markers in the cluster relative to the mean const isLargeCluster = count > Math.max(10, stats.clusters.markers.mean); const clusterClass = isLargeCluster ? 'large-cluster' : 'small-cluster'; // Build the HTML content for the Advanced Marker const content = document.createElement('div'); content.classList.add('custom-cluster-icon', clusterClass); content.textContent = String(count); return new AdvancedMarkerElement({ position, content, // Adjust zIndex to be above other markers zIndex: 1000 + count, }); }, }; // Add a marker clusterer to manage the markers. new MarkerClusterer({ markers, map: innerMap, renderer }); } const locations = [ { lat: -31.56391, lng: 147.154312 }, { lat: -33.718234, lng: 150.363181 }, { lat: -33.727111, lng: 150.371124 }, { lat: -33.848588, lng: 151.209834 }, { lat: -33.851702, lng: 151.216968 }, { lat: -34.671264, lng: 150.863657 }, { lat: -35.304724, lng: 148.662905 }, { lat: -36.817685, lng: 175.699196 }, { lat: -36.828611, lng: 175.790222 }, { lat: -37.75, lng: 145.116667 }, { lat: -37.759859, lng: 145.128708 }, { lat: -37.765015, lng: 145.133858 }, { lat: -37.770104, lng: 145.143299 }, { lat: -37.7737, lng: 145.145187 }, { lat: -37.774785, lng: 145.137978 }, { lat: -37.819616, lng: 144.968119 }, { lat: -38.330766, lng: 144.695692 }, { lat: -39.927193, lng: 175.053218 }, { lat: -41.330162, lng: 174.865694 }, { lat: -42.734358, lng: 147.439506 }, { lat: -42.734358, lng: 147.501315 }, { lat: -42.735258, lng: 147.438 }, { lat: -43.999792, lng: 170.463352 }, ]; void init();
CSS
html, body, gmp-map { height: 100%; margin: 0; padding: 0; } .custom-cluster-icon { width: 45px; height: 45px; border-radius: 50%; border: 2px solid white; display: flex; justify-content: center; align-items: center; color: white; font-weight: bold; font-size: 14px; font-family: sans-serif; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3); } .large-cluster { background-color: #ff0000; } .small-cluster { background-color: #0000ff; }
HTML
<html>
<head>
<title>marker-clustering-custom</title>
<link rel="stylesheet" type="text/css" href="./style.css" />
<script type="module" src="./index.js"></script>
<script>
// prettier-ignore
(g=>{var h,a,k,p="The Google Maps JavaScript API",c="google",l="importLibrary",q="__ib__",m=document,b=window;b=b[c]||(b[c]={});var d=b.maps||(b.maps={}),r=new Set,e=new URLSearchParams,u=()=>h||(h=new Promise(async(f,n)=>{await (a=m.createElement("script"));e.set("libraries",[...r]+"");for(k in g)e.set(k.replace(/[A-Z]/g,t=>"_"+t[0].toLowerCase()),g[k]);e.set("callback",c+".maps."+q);a.src=`https://maps.${c}apis.com/maps/api/js?`+e;d[q]=f;a.onerror=()=>h=n(Error(p+" could not load."));a.nonce=m.querySelector("script[nonce]")?.nonce||"";m.head.append(a)}));d[l]?console.warn(p+" only loads once. Ignoring:",g):d[l]=(f,...n)=>r.add(f)&&u().then(()=>d[l](f,...n))})({
key: "GOOGLE_MAPS_API_KEY"
});
</script>
</head>
<body>
<gmp-map
center="-28.024, 140.887"
zoom="3"
map-id="DEMO_MAP_ID"></gmp-map>
</body>
</html>추가 렌더러 예시는 렌더러 사용 데모를 참고하세요.
클러스터링 알고리즘 수정
기본적으로 MarkerClusterer는 k-d 트리를 사용하여 마커를 클러스터링하는 SuperClusterAlgorithm을 사용합니다. 알고리즘 인스턴스를
algorithm 옵션에 전달하거나 기본 알고리즘의 옵션을 algorithmOptions에 전달하여 클러스터링 동작을 맞춤설정할 수 있습니다.
라이브러리에는 다음과 같은 기본 제공 알고리즘이 포함되어 있습니다.
SuperClusterAlgorithm(기본값): k-d 트리를 사용하는 빠른 지리공간 포인트 클러스터링입니다. 일반적인 옵션은 다음과 같습니다.maxZoom: 클러스터가 생성되는 최대 확대/축소 수준입니다 (기본값:16).maxZoom보다 높은 확대/축소 수준에서는 클러스터링 없이 개별 마커가 표시됩니다.radius: 클러스터 반지름(픽셀)(기본값:60).minPoints: 클러스터를 형성하는 데 필요한 최소 마커 수입니다 (기본값:2).
SuperClusterViewportAlgorithm: 현재 지도 뷰포트 내의 마커만 클러스터링하는SuperClusterAlgorithm의 뷰포트 필터링된 변형입니다 (픽셀 패딩viewportPadding, 기본값:60).GridAlgorithm: 그리드 기반 클러스터링 알고리즘입니다. 일반적인 옵션에는maxZoom(기본값:16),gridSize(그리드 크기(픽셀), 기본값:40),maxDistance(클러스터 중심과 마커 간의 최대 거리(미터), 기본값:40000)가 포함됩니다.NoopAlgorithm: 클러스터링 없이 모든 마커를 렌더링합니다.
다음 예에서는 맞춤 radius 및 maxZoom으로 SuperClusterAlgorithm을 구성합니다.
import {MarkerClusterer, SuperClusterAlgorithm} from '@googlemaps/markerclusterer'; const markerCluster = new MarkerClusterer({ map, markers, algorithm: new SuperClusterAlgorithm({ radius: 80, maxZoom: 15, }), });
알고리즘의 동작 방식을 비교하려면 알고리즘 비교 데모를 참고하세요.
클러스터 클릭 동작 맞춤설정
기본적으로 클러스터 마커를 클릭하면 지도 뷰포트가 클릭된 클러스터의 경계에 맞춰집니다
(map.fitBounds(cluster.bounds)). 사용자가 클러스터를 클릭할 때 발생하는 작업을 맞춤설정하려면 콜백 함수를
onClusterClick
옵션에 전달합니다.
const markerCluster = new MarkerClusterer({ map, markers, onClusterClick: (event, cluster, map) => { // Zoom in by 2 levels centered on the cluster position map.setCenter(cluster.position); map.setZoom((map.getZoom() ?? 3) + 2); }, });
자세히 알아보기
GitHub 저장소에서 마커 클러스터링의 더 복잡한 예를 확인하고 라이브러리에 대한 참조 문서를 읽어 보세요.