로컬 탐색 제품은 결국 화면 하나에서 성패가 갈립니다. 바로 지도예요. 사용자가 앱을 여는 이유는 단순한 질문 하나 때문입니다. "지금 내 근처에서 뭐 하지?" 그리고 그 답을 찾는 수단이 지도죠. 정확한 핀, 빠른 필터링, 그리고 검색 엔진이 실제로 읽을 수 있는 리스팅까지 갖춘 지도를 제대로 만드는 것, 이게 모든 이벤트 가이드 뒤에 깔린 핵심 엔지니어링 작업입니다.
이 가이드에서는 도시 이벤트 지도를 바닥부터 만들어 봅니다. 이벤트 데이터 모델링, 장소를 좌표로 geocoding하기, 마커 찍고 clustering하기, 카테고리와 동네별 필터링, 그리고 리스팅을 검색에 노출시키기까지요. 여러분이 base map 렌더링은 이미 할 줄 안다고 가정합니다. 아직 모르겠다면 웹사이트에 인터랙티브 지도 추가하는 법 튜토리얼부터 보고, 이벤트에 특화된 부분은 여기서 이어가면 됩니다.
이벤트를 장소 중심으로 모델링하기
이벤트는 어떤 장소에서 특정 시간에 일어나는 무언가입니다. 지도에 올라가는 건 그 장소니까, 데이터 모델의 앵커도 장소가 되어야 해요. 최소한의 이벤트 레코드는 이렇게 생겼습니다:
const event = {
id: 'evt_8471',
title: 'Late Night Jazz at The Vortex',
category: 'live-music',
venue: 'The Vortex Jazz Club, 11 Gillett Square, London N16 8AZ',
coords: null, // geocoding으로 딱 한 번 채워짐
neighbourhood: null, // reverse geocoding으로 딱 한 번 채워짐
startsAt: '2026-07-09T20:30:00Z',
url: 'https://example.com/tickets/8471',
};
두 개의 null 필드가 중요해요. 지도가 로드될 때마다 장소를 geocoding하고 싶진 않잖아요. 이벤트가 생성되거나 임포트될 때 한 번만 geocoding하고, 그 좌표를 저장한 다음, 영구적인 값으로 취급하세요.
Step 1: 각 장소를 좌표로 geocoding하기
geocoding은 사람이 읽는 주소를 지도에 찍을 수 있는 위도/경도로 바꿔줍니다. 장소 문자열을 Geocoding API에 보내고 최상위 결과에서 좌표를 읽어오면 돼요:
async function geocodeVenue(address) {
const url = new URL('https://api.mapatlas.eu/geocoding/v1/search');
url.searchParams.set('text', address);
url.searchParams.set('size', '1');
const res = await fetch(url, { headers: { Authorization: `Bearer ${API_KEY}` } });
const data = await res.json();
const top = data.features?.[0];
if (!top) return null;
const [lon, lat] = top.geometry.coordinates;
return { lat, lon, label: top.properties.label };
}
이 작업은 write time에, 즉 이벤트가 시스템에 들어올 때 실행하고, 반환된 좌표를 레코드에 저장해 두세요. 왜 정확한 핀 위치가 중요한지, 그리고 옥상 좌표가 사람들이 실제로 걸어 들어가는 출입문과 어떻게 다른지에 대한 자세한 설명은 건물 출입구 geocoding 글을 참고하세요.
Step 2: 이벤트를 마커로 찍기
모든 이벤트에 좌표가 있으면, 찍는 건 그냥 루프입니다. 지도를 한눈에 읽을 수 있게 카테고리마다 고유한 색을 주세요:
const CATEGORY_COLORS = {
'live-music': '#E75480',
'theatre': '#006BA6',
'comedy': '#EE7C0E',
'food': '#16A34A',
'nightlife': '#7C3AED',
};
function plotEvents(map, events) {
events.forEach((evt) => {
if (!evt.coords) return;
const popup = new mapmetricsgl.Popup().setHTML(
`<strong>${evt.title}</strong><br>${new Date(evt.startsAt).toLocaleString()}` +
`<br><a href="${evt.url}">티켓</a>`,
);
new mapmetricsgl.Marker({ color: CATEGORY_COLORS[evt.category] || '#0c3456' })
.setLngLat([evt.coords.lon, evt.coords.lat])
.setPopup(popup)
.addTo(map);
});
}
이게 실제 제품 뒤에 깔린 패턴이에요. 실제 사례로 OnlyHere가 있는데, 런던에서 할 만한 일들을 매일 안내하는 가이드입니다. 모든 장소를 geocoding해서 그날의 이벤트를 인터랙티브 MapAtlas 지도에 찍어주죠. 덕분에 사용자는 밋밋한 리스트를 스크롤하는 대신 장소와 동네별로 뭐가 있는지 둘러볼 수 있습니다.
Step 3: 도시가 꽉 차면 clustering하기
이벤트가 몇 개 안 될 때는 개별 마커로 충분해요. 하지만 도시 전체 피드는 몇 개짜리가 아니죠. 핀이 대략 100~200개를 넘어가면, 이벤트를 GeoJSON source로 옮기고 지도가 clustering하게 두세요:
map.addSource('events', {
type: 'geojson',
cluster: true,
clusterRadius: 50,
data: {
type: 'FeatureCollection',
features: events.filter((e) => e.coords).map((e) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
properties: { title: e.title, category: e.category },
})),
},
});
이제 도심 한복판의 바쁜 밤도 수백 개의 겹친 핀 대신 카운트가 적힌 버블 하나로 표시되고, 사용자가 줌인하면 그 cluster가 다시 흩어집니다.
Step 4: 카테고리와 동네별로 필터링하기
필터링이야말로 이벤트 지도가 이벤트 제품으로 거듭나는 지점이에요. 전체 리스트를 메모리에 들고 배열을 필터링한 다음, 마커를 손으로 다시 만드는 게 아니라 필터링된 결과를 map source에 넘겨주세요:
function applyFilters(map, allEvents, { category, neighbourhood }) {
const filtered = allEvents.filter((e) =>
(!category || e.category === category) &&
(!neighbourhood || e.neighbourhood === neighbourhood),
);
map.getSource('events').setData({
type: 'FeatureCollection',
features: filtered.filter((e) => e.coords).map((e) => ({
type: 'Feature',
geometry: { type: 'Point', coordinates: [e.coords.lon, e.coords.lat] },
properties: { title: e.title },
})),
});
return filtered;
}
neighbourhood 필드를 채우려면, 각 장소의 좌표를 한 번 reverse geocoding해서 admin 영역 이름을 저장해 두세요. 그 값 하나가 동네 드롭다운, "이 동네에서 뭐 해" 뷰, 그리고 더 깔끔한 리스팅 페이지까지 전부 돌아가게 해줍니다. 런타임에 별도 조회를 또 할 필요 없이요.
Step 5: 리스팅을 검색에 노출시키기
지도는 이미 여러분 앱 안에 들어와 있는 사람한테만 답을 줍니다. 검색 엔진이나 AI 어시스턴트에 "오늘 밤 Shoreditch에서 뭐 해"라고 묻는, 훨씬 더 큰 규모의 사람들한테는 아무것도 해주지 못하죠. 그 시스템들은 픽셀이 아니라 structured data를 읽습니다.
모든 이벤트를 schema.org Event 필드로 마크업하고, 이미 갖고 있는 좌표를 재사용하세요:
{
"@context": "https://schema.org",
"@type": "Event",
"name": "Late Night Jazz at The Vortex",
"startDate": "2026-07-09T20:30:00Z",
"location": {
"@type": "Place",
"name": "The Vortex Jazz Club",
"geo": { "@type": "GeoCoordinates", "latitude": 51.5462, "longitude": -0.0753 }
}
}
핀을 그리는 그 좌표가 structured data에도 그대로 들어가니까, 표시와 탐색이 하나의 source of truth에서 나옵니다. 이게 보기 좋기만 한 지도와, 실제로 검색에 걸리는 제품의 차이예요. 기계가 읽을 수 있는 위치 데이터가 왜 AI 검색 노출을 끌어올리는지 더 알고 싶다면 AI 검색을 위한 위치 특화 FAQ 가이드를 참고하세요.
정리하며
도시 이벤트 지도는 다섯 개의 움직이는 부품으로 이뤄집니다. 장소를 앵커로 삼는 데이터 모델, write time의 geocoding, 색깔 있는 마커, 규모에 맞춘 clustering, 그리고 배열 기반 필터링. 이 모두가 리스팅을 계속 검색에 노출시켜 주는 structured data 위에 얹혀 있죠. 이 부품들을 하나의 geospatial 플랫폼 위에서 만들면, 좌표가 geocoder에서 지도로, 다시 schema로 깔끔하게 흘러갑니다. 중복 조회도 없고, 데이터가 어긋날 일도 없이요.
MapAtlas는 이 모든 걸 EU에 호스팅된 GDPR 준수 인프라 위에서 만들 수 있도록 geocoding, reverse geocoding, 장소 검색, 지도 렌더링을 제공합니다. Geocoding API와 Search API를 둘러보고 여러분만의 도시를 찍어 보세요.
자주 묻는 질문
도시 이벤트 지도를 만들려면 어떤 데이터가 필요한가요?
최소한 이벤트마다 장소 주소, 카테고리, 시작 시간이 있어야 해요. 이 중에서 주소가 이벤트를 지도 핀으로 바꿔주는 핵심입니다. 주소를 한 번 geocoding해서 위도와 경도를 얻고, 그 좌표를 저장한 뒤, 이벤트를 보여줄 때마다 재사용하면 됩니다. 티켓 링크, 이미지, 설명 같은 나머지는 전부 마커 popup에 붙이는 metadata예요.
장소 주소를 지도 좌표로 어떻게 바꾸나요?
geocoding API에 장소의 주소 문자열을 보내고, 응답에서 위도와 경도를 읽어오면 됩니다. MapAtlas Geocoding API를 쓴다면 주소를 텍스트 쿼리로 https://api.mapatlas.eu/geocoding/v1/search 에 보내고, 최상위 결과의 좌표를 사용하세요. 페이지가 로드될 때마다가 아니라 이벤트가 생성될 때 각 장소를 딱 한 번만 geocoding하는 게 포인트예요. 그래야 지도도 빠르게 유지되고 API 사용량도 낮게 유지됩니다.
이벤트 마커는 몇 개까지 찍어야 지도가 느려지지 않나요?
개별 마커를 찍는 방식은 줌을 뺀 화면에서 대략 100~200개 핀까지는 부드럽게 동작해요. 그 이상이 되면 GeoJSON source clustering으로 넘어가야 합니다. clustering은 낮은 줌에서 가까운 이벤트들을 하나의 카운트 버블로 묶어주고, 사용자가 줌인하면 다시 흩어놓죠. 리스팅이 수백 개나 되는 도시 전체 이벤트 피드라면 처음부터 clustering을 쓰는 게 맞습니다.
사용자가 카테고리나 동네별로 이벤트를 필터링하게 하려면 어떻게 하나요?
전체 이벤트 리스트를 배열로 메모리에 들고 있으면서, 각 항목에 카테고리와 동네를 붙여두고, 사용자의 선택에 따라 그 배열을 필터링하세요. 그런 다음 마커를 다 지웠다가 다시 만드는 게 아니라, 필터링된 결과로 map source를 업데이트하면 됩니다. 동네별로 둘러보는 것도 원리는 같아요. 좌표가 선택한 영역 안에 들어오는 이벤트, 또는 reverse geocoding으로 얻은 같은 admin 이름을 공유하는 이벤트만 필터링하면 됩니다.
이벤트 지도에 왜 structured data가 필요한가요?
시각적인 지도는 사람한테 도움이 되지만, 검색 엔진과 AI 어시스턴트는 structured data를 읽어요. 각 이벤트를 schema.org Event 필드로 마크업하면서 장소 이름, 좌표, 시작 시간을 넣어두는 것이, '오늘 밤 Shoreditch에서 뭐 해' 같은 질문의 답변에 리스팅이 뜨게 만드는 핵심입니다. 지도에 찍는 그 좌표가 structured data에도 그대로 들어가니까, 탐색과 표시가 하나의 source of truth에서 나오게 되는 거죠.

