모든 소매 브랜드, 프랜차이즈 및 서비스 비즈니스는 결국 매장 찾기 기능이 필요합니다. 고객이 이미 구매하려고 할 때 방문하는 페이지이며, 어느 지점으로 들어갈지만 알면 됩니다. 그 페이지를 잘못 얻으면 실제 전환이 손실됩니다. 올바르게 하는 것은 대부분의 개발자들이 예상하는 것보다 간단합니다.
표준 접근 방식은 Google Maps였지만, 규모에서 물린다는 비용 구조가 있습니다. 50,000명의 월간 방문자가 지도 페이지를 로드하는 바쁜 소매 사이트는 하룻밤 사이에 수백 유로의 지도 로드 및 지오코딩 요금을 누적할 수 있습니다. 이제 프로덕션 사용에 대한 무료 계층이 없으며, 청구는 불명확하고, 미국 기반 매핑 서비스를 사용하는 EU 비즈니스의 GDPR 상황은 상단에 규정 준수 오버헤드를 추가합니다.
이 튜토리얼은 MapAtlas Maps API 및 Geocoding API를 사용하여 완전한 매장 찾기 기능, 대화형 지도, 주소 검색, 동기화된 목록 패널, 마커 클러스터링 및 클릭-세부 팝업을 구축합니다. 프레임워크 필요 없음. 완전한 작동 예제는 대략 80줄의 HTML 및 JavaScript에 맞습니다. 이를 WordPress 사용자 정의 HTML 블록, Shopify 섹션 또는 이를 읽으신 같은 날 오후의 모든 CMS에 삽입할 수 있습니다.
마지막에는 다음을 갖게 됩니다:
- 매장 네트워크 중심의 렌더링된 벡터 지도
- 낮은 줌에서 클러스터링으로 JSON 데이터 배열에서 로드된 마커
- 지오코딩 API로 구동되는 주소 검색 표시줄
- 지도 클릭 시 강조되는 동기화된 목록 패널
- 모바일 반응형 2열 레이아웃
매장 찾기 기능이 실제로 필요한 것
코드를 작성하기 전에 요구사항을 정확히 하는 것이 도움됩니다. 작동하는 매장 찾기 기능에는 4가지 이동 부분이 있습니다:
- 지도: 타일을 렌더링하고 패닝 및 줌을 수용하며 마커를 표시합니다.
- 검색 입력: 사용자의 입력된 주소를 좌표로 지오코드한 다음 지도를 다시 중앙화합니다.
- 마커 레이어: 각 매장 위치를 표시하고, 낮은 줌에서 근처 핀을 클러스터링하며, 클릭 시 세부 팝업을 엽니다.
- 목록 패널: 검색된 위치로부터의 거리로 정렬된 매장을 표시하고, 활성 매장을 강조하며, 지도와 동기화하여 스크롤합니다.
그게 다입니다. 방향, 영업 시간, 재고와 같은 다른 모든 기능은 이 4가지 위에 계층화된 개선 사항입니다. 먼저 핵심을 구축하세요.
1단계: MapAtlas SDK 로드
MapAtlas Maps API는 Mapbox GL JS 인터페이스와 호환되므로 모든 Mapbox GL JS 튜토리얼 또는 플러그인이 직접 작동합니다. 페이지 <head>에 CDN 링크를 추가하세요:
<link
rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css"
/>
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
npm을 사용하는 경우:
npm install @mapmetrics/mapmetrics-gl
portal.mapmetrics.org/signup에서 무료 API 키를 가져오세요. 한 개의 키는 지도 타일, 지오코딩 및 경로 지정을 포함하며, 따로 분리된 자격 증명이 필요하지 않습니다.
2단계: 매장 데이터 정의
매장 데이터는 GeoJSON FeatureCollection입니다. 각 기능은 매장의 좌표와 팝업이 필요한 모든 속성(이름, 주소, 전화, 영업 시간)을 포함합니다.
const stores = {
type: "FeatureCollection",
features: [
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.9041, 52.3676] },
properties: {
id: 1,
name: "Amsterdam Central",
address: "Stationsplein 12, 1012 AB Amsterdam",
phone: "+31 20 123 4567",
hours: "Mon-Sat 09:00-20:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [4.4777, 51.9244] },
properties: {
id: 2,
name: "Rotterdam Lijnbaan",
address: "Lijnbaan 10, 3012 EL Rotterdam",
phone: "+31 10 987 6543",
hours: "Mon-Sat 09:00-21:00"
}
},
{
type: "Feature",
geometry: { type: "Point", coordinates: [5.1214, 52.0907] },
properties: {
id: 3,
name: "Utrecht Centrum",
address: "Oudegracht 45, 3511 AB Utrecht",
phone: "+31 30 555 1234",
hours: "Mon-Sun 10:00-19:00"
}
}
]
};
프로덕션에서는 API 끝점 또는 CMS에서 이를 가져올 것입니다. 구조는 동일하게 유지되며, 유일한 차이는 데이터의 출처입니다.
3단계: 지도 렌더링 및 클러스터링 추가
지도를 초기화하고, 클러스터링이 활성화된 GeoJSON 소스로 매장 데이터를 추가하며, 클러스터 원과 개별 핀 레이어를 칠합니다. Mapbox GL JS 클러스터링은 소스 정의에 내장되어 있으며 플러그인이 필요하지 않습니다.
const map = new mapmetricsgl.Map({
container: 'map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [5.2913, 52.1326], // Centre of the Netherlands
zoom: 7
});
map.on('load', () => {
// Add GeoJSON source with clustering
map.addSource('stores', {
type: 'geojson',
data: stores,
cluster: true,
clusterMaxZoom: 12,
clusterRadius: 50
});
// Cluster circles
map.addLayer({
id: 'clusters',
type: 'circle',
source: 'stores',
filter: ['has', 'point_count'],
paint: {
'circle-color': '#3B82F6',
'circle-radius': ['step', ['get', 'point_count'], 20, 10, 28, 30, 36]
}
});
// Cluster count labels
map.addLayer({
id: 'cluster-count',
type: 'symbol',
source: 'stores',
filter: ['has', 'point_count'],
layout: {
'text-field': '{point_count_abbreviated}',
'text-size': 13
},
paint: { 'text-color': '#ffffff' }
});
// Individual store pins
map.addLayer({
id: 'unclustered-point',
type: 'circle',
source: 'stores',
filter: ['!', ['has', 'point_count']],
paint: {
'circle-color': '#EF4444',
'circle-radius': 8,
'circle-stroke-width': 2,
'circle-stroke-color': '#ffffff'
}
});
});
클러스터를 클릭하면 개별 매장을 드러내기 위해 확대됩니다. 클러스터되지 않은 핀을 클릭하면 팝업이 열립니다.
Step 4: Wire Up Popups and the List Panel
When a user clicks a store pin, show a popup on the map and highlight the matching card in the list panel. Both interactions should be bidirectional, clicking a list card should also fly the map to that store.
// Click unclustered store → open popup + highlight list card
map.on('click', 'unclustered-point', (e) => {
const { coordinates } = e.features[0].geometry;
const { name, address, phone, hours, id } = e.features[0].properties;
new mapmetricsgl.Popup()
.setLngLat(coordinates)
.setHTML(`
<strong>${name}</strong>
<p>${address}</p>
<p>${phone}</p>
<p>${hours}</p>
`)
.addTo(map);
highlightCard(id);
});
// Click cluster → zoom in
map.on('click', 'clusters', (e) => {
const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
const clusterId = features[0].properties.cluster_id;
map.getSource('stores').getClusterExpansionZoom(clusterId, (err, zoom) => {
if (err) return;
map.easeTo({ center: features[0].geometry.coordinates, zoom });
});
});
function highlightCard(id) {
document.querySelectorAll('.store-card').forEach(card => {
card.classList.toggle('active', card.dataset.id === String(id));
});
}
// Build list panel from store data
function buildListPanel() {
const list = document.getElementById('store-list');
stores.features.forEach(({ properties, geometry }) => {
const card = document.createElement('div');
card.className = 'store-card';
card.dataset.id = properties.id;
card.innerHTML = `
<strong>${properties.name}</strong>
<p>${properties.address}</p>
<small>${properties.hours}</small>
`;
card.addEventListener('click', () => {
map.flyTo({ center: geometry.coordinates, zoom: 14 });
highlightCard(properties.id);
});
list.appendChild(card);
});
}
Step 5: Add Address Search with the Geocoding API
The search bar takes a user's typed location, geocodes it via the Geocoding API, flies the map to that point, and re-sorts the list panel by distance.
async function searchLocation(query) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', query);
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.features.length) {
alert('Address not found. Try a city or postcode.');
return;
}
const [lng, lat] = data.features[0].geometry.coordinates;
// Fly map to searched location
map.flyTo({ center: [lng, lat], zoom: 10 });
// Sort list by distance from searched point
const sorted = [...stores.features].sort((a, b) => {
const distA = haversine(lat, lng, a.geometry.coordinates[1], a.geometry.coordinates[0]);
const distB = haversine(lat, lng, b.geometry.coordinates[1], b.geometry.coordinates[0]);
return distA - distB;
});
document.getElementById('store-list').innerHTML = '';
sorted.forEach(feature => {
// Re-render each card (reuse buildListPanel logic)
});
}
function haversine(lat1, lon1, lat2, lon2) {
const R = 6371;
const dLat = ((lat2 - lat1) * Math.PI) / 180;
const dLon = ((lon2 - lon1) * Math.PI) / 180;
const a =
Math.sin(dLat / 2) ** 2 +
Math.cos((lat1 * Math.PI) / 180) *
Math.cos((lat2 * Math.PI) / 180) *
Math.sin(dLon / 2) ** 2;
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}
document.getElementById('search-btn').addEventListener('click', () => {
const query = document.getElementById('search-input').value.trim();
if (query) searchLocation(query);
});
Step 6: Mobile-Responsive Layout
A store locator on mobile must stack vertically, map on top, list below, rather than side by side. Twenty lines of CSS handles this with a single media query breakpoint.
#locator-wrapper {
display: flex;
height: 600px;
gap: 0;
}
#store-list {
width: 300px;
overflow-y: auto;
border-right: 1px solid #e5e7eb;
padding: 12px;
}
#map {
flex: 1;
}
.store-card {
padding: 12px;
border-radius: 8px;
cursor: pointer;
margin-bottom: 8px;
border: 2px solid transparent;
transition: border-color 0.2s;
}
.store-card.active {
border-color: #3B82F6;
background: #EFF6FF;
}
@media (max-width: 640px) {
#locator-wrapper {
flex-direction: column;
height: auto;
}
#store-list {
width: 100%;
border-right: none;
border-top: 1px solid #e5e7eb;
height: 280px;
}
#map {
height: 350px;
}
}
Billing and GDPR Comparison with Google Maps
If you have been running Google Maps on a retail site and wondering why this month's bill came in higher than expected, you are not alone. The Maps JavaScript API charges per map load. The Places API charges per autocomplete session and per geocoding request. Those costs compound fast. A site doing 50,000 visits a month, each loading the store locator page once, spends around €140/month on map loads alone before a single geocoding call.
MapAtlas uses flat monthly plans. There is no per-load or per-request charge that spikes without warning. You can read the full breakdown in Google Maps API Pricing in 2026: The True Cost Breakdown and the MapAtlas vs. Google Maps comparison.
For EU developers the GDPR angle matters too. Google Maps routes data through US infrastructure. MapAtlas is EU-hosted, ISO 27001 certified, and processes all requests within the EU. For retail businesses that are already managing customer consent carefully, using an EU-native mapping provider removes one more third-party transfer from your privacy policy.
Putting It All Together
The complete store locator, HTML structure, CSS layout, map init, clustering, popup handling, list panel, search, and distance sort, fits comfortably in one file. The structure looks like this:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Store Locator</title>
<link rel="stylesheet"
href="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css" />
<script src="https://unpkg.com/@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.js"></script>
<style>
/* paste the CSS from Step 6 here */
</style>
</head>
<body>
<div id="search-bar">
<input id="search-input" type="text" placeholder="Enter your city or postcode…" />
<button id="search-btn">Search</button>
</div>
<div id="locator-wrapper">
<div id="store-list"></div>
<div id="map"></div>
</div>
<script>
// paste store data, map init, clustering, popup, list panel, and search from Steps 2–5
</script>
</body>
</html>
The result is a production-ready store locator with zero external dependencies beyond the MapAtlas SDK. There is no build step, no framework, and no ongoing billing surprises.
If you need to add routing, "get directions from my location to this store", the Routing API takes the user's coordinates and the store's coordinates and returns a full turn-by-turn route you can draw on the map as a line layer. The How to Add Interactive Maps to Your Website tutorial covers that next step in detail.
Next Steps
- Sign up for a free MapAtlas API key, no credit card required
- Browse the Maps API documentation for clustering, custom styling, and layer options
- Explore the Geocoding API for postcode lookup, reverse geocoding, and address autocomplete
Frequently Asked Questions
Can I build a store locator without Google Maps?
Yes. MapAtlas provides a Mapbox GL JS-compatible Maps API and a Geocoding API that cover every feature a store locator needs, interactive map, address search, marker clustering, and popups, with no per-load billing and full GDPR compliance.
How much does a store locator cost to run on MapAtlas vs Google Maps?
MapAtlas is roughly 75% cheaper than Google Maps for equivalent usage. Google Maps charges per map load and per geocoding request, which adds up fast on a busy retail site. MapAtlas uses flat monthly plans with no per-request surprises.
Does MapAtlas work on WordPress and Shopify?
Yes. Because MapAtlas is pure JavaScript with no framework dependency, you can embed it in a WordPress custom HTML block, a Shopify theme section, or any CMS that lets you add a script tag and a div.

