추적 지도는 모든 음식 배달 또는 라이드셰어 앱에서 가장 높은 사용자 참여도를 가진 화면입니다. 고객들이 대기하는 동안 계속 보는 화면이 바로 이것입니다. 부드러운 움직임, 정확한 ETA, 명확한 경로선을 올바르게 구현하는 것이 전문적으로 느껴지는 앱과 신뢰할 수 없어 보이는 앱의 차이를 결정합니다.
이 튜토리얼은 고객이 보는 추적 지도를 처음부터 만듭니다. WebSocket을 통해 GPS 위치를 브로드캐스트하는 백엔드, 이를 받아 드라이버 마커를 점프 없이 움직이는 프론트엔드, MapAtlas Routing API의 경로선, 그리고 실시간 ETA 표시를 포함합니다. 완전한 구현은 70줄 미만의 클라이언트 측 JavaScript로 작성되었으며, WebSocket 메시지를 보낼 수 있는 모든 백엔드와 통합할 수 있도록 설계되었습니다.
이 아키텍처는 음식 배달, 식료품 배달, 라이드셰어, 야외 서비스, 그리고 차량이 고정된 목적지로 향하고 고객이 실시간으로 이를 지켜보는 모든 사용 사례에서 작동합니다.
아키텍처 개요
코드를 작성하기 전에 데이터 흐름을 이해하는 것이 도움이 됩니다:
- 드라이버 앱(모바일, GPS 하드웨어)은 3~5초마다 위도/경도를 백엔드로 전송합니다.
- 백엔드(Node.js, Python, Go 등)는 마지막 알려진 위치를 저장하고 WebSocket을 통해 연결된 모든 주문 구독자에게 브로드캐스트합니다.
- 고객 브라우저는 WebSocket 메시지를 받고 보간된 애니메이션을 사용하여 지도 위의 마커를 움직입니다.
- Routing API는 주문이 생성될 때 한 번 호출되어 계획된 경로를 가져옵니다. 디코딩된 폴리라인은 라인 레이어로 표시됩니다.
- ETA는 남은 거리와 평균 속도를 비교하거나 드라이버의 현재 위치에서 Routing API를 다시 호출하여 재계산됩니다.
백엔드 구현은 이 튜토리얼의 범위를 벗어나지만, 이 형식으로 메시지를 전송하는 모든 WebSocket 서버는 아래의 프론트엔드 코드와 작동합니다:
{
"type": "position_update",
"orderId": "order-8821",
"lat": 52.3741,
"lng": 4.8952,
"heading": 92,
"speed": 28,
"timestamp": 1738234521000
}
단계 1: 지도 초기화
배달 출발지 중심으로 지도를 설정합니다. Routing API 호출은 단계 4에서 발생하므로 지금은 캔버스만 초기화합니다.
import mapmetricsgl from '@mapmetrics/mapmetrics-gl';
import '@mapmetrics/mapmetrics-gl/dist/mapmetrics-gl.css';
const map = new mapmetricsgl.Map({
container: 'tracking-map',
style: 'https://tiles.mapatlas.eu/styles/basic/style.json?key=YOUR_API_KEY',
center: [4.9041, 52.3676],
zoom: 14
});
// Destination marker (restaurant or pickup point)
const destination = [4.9001, 52.3791];
new mapmetricsgl.Marker({ color: '#EF4444' })
.setLngLat(destination)
.setPopup(new mapmetricsgl.Popup().setHTML('<strong>Pickup point</strong>'))
.addTo(map);
단계 2: 방향 회전이 있는 드라이버 마커
드라이버 마커를 별도로 생성하여 각 GPS 신호에서 위치를 업데이트할 수 있습니다. 사용자 정의 HTML 요소를 사용하면 마커 아이콘을 회전하여 드라이버의 방향을 반영할 수 있으며, 이는 추적이 훨씬 더 현실적으로 느껴지게 합니다.
// Custom element so we can rotate it
const driverEl = document.createElement('div');
driverEl.innerHTML = '🚗';
driverEl.style.cssText = 'font-size:28px;transform-origin:center;transition:transform 0.3s';
const driverMarker = new mapmetricsgl.Marker({ element: driverEl, anchor: 'center' })
.setLngLat([4.9041, 52.3676])
.addTo(map);
function setDriverHeading(heading) {
driverEl.style.transform = `rotate(${heading}deg)`;
}
단계 3: WebSocket 연결 및 부드러운 보간
이것은 추적 지도의 핵심입니다. WebSocket에 연결하는 것은 한 줄이지만, GPS 신호 간에 마커 위치를 보간하여 순간이동하지 않고 부드럽게 미끄러지게 하는 것이 중요한 부분입니다.
let prevPos = null; // { lat, lng }
let animFrame = null;
function interpolateMarker(fromLat, fromLng, toLat, toLng, durationMs) {
const startTime = performance.now();
function step(now) {
const elapsed = now - startTime;
const t = Math.min(elapsed / durationMs, 1); // 0 → 1
const lat = fromLat + (toLat - fromLat) * t;
const lng = fromLng + (toLng - fromLng) * t;
driverMarker.setLngLat([lng, lat]);
if (t < 1) {
animFrame = requestAnimationFrame(step);
}
}
if (animFrame) cancelAnimationFrame(animFrame);
animFrame = requestAnimationFrame(step);
}
const ws = new WebSocket('wss://your-backend.example.com/track/order-8821');
ws.addEventListener('message', (event) => {
const msg = JSON.parse(event.data);
if (msg.type !== 'position_update') return;
const { lat, lng, heading } = msg;
if (prevPos) {
// Smooth animation from previous to new position
interpolateMarker(prevPos.lat, prevPos.lng, lat, lng, 400);
} else {
// First ping, place marker immediately
driverMarker.setLngLat([lng, lat]);
map.flyTo({ center: [lng, lat], zoom: 15 });
}
setDriverHeading(heading);
updateETA(lat, lng);
prevPos = { lat, lng };
});
ws.addEventListener('close', () => {
console.log('Driver has arrived or connection closed.');
});
400ms 보간 윈도우는 일반적인 3~5초의 GPS 신호 간격과 적절히 일치하며, 마커는 항상 실제보다 약간 뒤지지만 눈에 띄게 점프하지 않습니다.
단계 4: Routing API에서 계획된 경로 그리기
주문이 할당될 때 전체 경로를 가져옵니다. 폴리라인 좌표를 저장하고 GeoJSON 라인 레이어로 그립니다. Route Optimization API 튜토리얼은 다중 정류장 시나리오를 다루며, 단순 A-B 배달의 경우 요청이 간단합니다.
async function fetchAndDrawRoute(originLat, originLng, destLat, destLng) {
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${originLat},${originLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (!data.routes?.length) return;
const route = data.routes[0];
map.on('load', () => {
map.addSource('route', {
type: 'geojson',
data: {
type: 'Feature',
geometry: route.geometry // GeoJSON LineString
}
});
map.addLayer({
id: 'route-line',
type: 'line',
source: 'route',
layout: { 'line-join': 'round', 'line-cap': 'round' },
paint: {
'line-color': '#3B82F6',
'line-width': 4,
'line-opacity': 0.8
}
});
});
return route.duration; // seconds
}
단계 5: ETA 계산 및 표시
드라이버의 현재 위치를 목적지와 비교하여 ETA를 계산합니다. 높은 정확도를 위해 30초마다 드라이버의 현재 위치에서 Routing API를 다시 호출하여 신선한 이동 시간 추정치를 얻습니다.
function haversineKm(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));
}
let lastRouteFetch = 0;
async function updateETA(driverLat, driverLng) {
const [destLng, destLat] = destination;
const distKm = haversineKm(driverLat, driverLng, destLat, destLng);
// Fast client-side estimate (assume 30 km/h urban average)
const minutesEstimate = Math.ceil((distKm / 30) * 60);
document.getElementById('eta').textContent =
distKm < 0.1
? 'Arriving now'
: `ETA: ${minutesEstimate} min (${distKm.toFixed(1)} km)`;
// Re-fetch from Routing API every 30 seconds for accuracy
const now = Date.now();
if (now - lastRouteFetch > 30_000) {
lastRouteFetch = now;
const url = new URL('https://api.mapatlas.eu/v1/routing/route');
url.searchParams.set('origin', `${driverLat},${driverLng}`);
url.searchParams.set('destination', `${destLat},${destLng}`);
url.searchParams.set('profile', 'driving');
url.searchParams.set('key', 'YOUR_API_KEY');
const res = await fetch(url);
const data = await res.json();
if (data.routes?.length) {
const mins = Math.ceil(data.routes[0].duration / 60);
document.getElementById('eta').textContent = `ETA: ${mins} min`;
}
}
}
드라이버 추적을 위한 GDPR 고려사항
드라이버 GPS 좌표는 GDPR 제4조(1)에 따른 개인 데이터입니다. 이 점에 대해 EU 음식 배달 및 라이드셰어 플랫폼을 지배하는 규정은 명확합니다:
데이터 최소화: 배송에 필요한 필드만 추적하세요 (위치, 방향, 속도). 운영상 필요한 것 이상으로 원시 GPS 히스토리를 기록하지 마세요.
보유 기한: 세밀한 이동 추적 데이터는 주문이 완료되면 삭제하거나 돌이킬 수 없게 익명화되어야 합니다. 집계된 경로 데이터(개인 드라이버와의 연계 없음)는 네트워크 최적화를 위해 더 오래 보유할 수 있습니다.
법적 근거: 제6조(1)(f)에 따른 정당한 이익은 실시간 배송 추적을 다룹니다. 추적 데이터의 부차적 사용(분석, 벤치마킹)에는 별도의 근거를 문서화해야 합니다.
드라이버 투명성: 드라이버 온보딩에 명확한 추적 공개를 포함하세요. 드라이버는 수집되는 내용, 보유 기간, 접근 가능한 사람이 무엇인지를 알아야 합니다.
데이터 거주지: MapAtlas는 모든 API 요청을 EU 내에서 처리합니다. 이는 미국 기반 지도 제공자에서 발생하는 제3국 이전 문제를 제거합니다. 전체 규정 준수 상황은 GDPR 준수 지도 API의 EU 개발자 가이드를 참조하세요.
라이드셰어링 및 이동성 산업 사용 사례에 대해 MapAtlas는 DPA 문서 및 EU 서버 보증을 표준으로 포함합니다. 물류 및 배달 산업 페이지는 차량 및 다중 드라이버 시나리오를 다룹니다.
프로덕션 강화
추적 기능을 고객에게 배포하기 전에 다음 항목을 확인하세요:
- WebSocket 재연결: 지수 백오프로
ws.addEventListener('close', reconnect)를 추가하세요. 모바일 네트워크는 자주 연결을 끊습니다. - 오래된 위치 처리: 15초 동안 업데이트가 도착하지 않으면 마지막 위치를 표시된 상태로 두지 말고 "드라이버 위치 파악 중" 상태를 표시하세요.
- 도착 감지:
distKm < 0.1일 때 "도착" 상태를 트리거하고 WebSocket을 닫은 후 확인 화면을 표시하세요. - 카메라가 드라이버를 따릅니다: 각 위치 업데이트에서
map.panTo([lng, lat])를 호출하여 드라이버를 중심에 유지합니다. 사용자가 지도를 탐색하려면 따릅니다 모드를 비활성화할 수 있는 "잠금" 토글을 제공합니다.
다음 단계
- 무료 MapAtlas API 키에 가입하고 구축을 시작합니다
- Route Optimization API 튜토리얼을 읽어 배달 앱에 다중 정류장 배송을 추가합니다
- Real Estate Property Map 튜토리얼을 탐색하여 동적 데이터 중심 지도 레이어의 또 다른 예를 확인합니다
자주 묻는 질문
드라이버의 위치를 지도에서 부드럽게 움직이는 방법은 무엇입니까?
GPS 신호는 몇 초마다 도착하여 마커 위치를 직접 업데이트하면 눈에 띄는 점프가 발생합니다. 부드러운 보간은 이전 위치와 새로운 위치 사이의 마커를 짧은 지속 시간(300~500ms) 동안 애니메이션하여, requestAnimationFrame을 사용하여 마커를 작은 증분으로 움직입니다. 이는 불규칙한 GPS 업데이트에도 불구하고 연속 이동의 모양을 제공합니다.
드라이버 위치 데이터는 GDPR의 적용을 받습니까?
예. 드라이버의 실시간 GPS 좌표는 GDPR 제4조에 따른 개인 데이터입니다. EU 음식 배달 및 라이드셰어 플랫폼은 보유를 최소화해야 하며, 추적 데이터는 이동이 완료되면 삭제하거나 익명화되어야 합니다. 처리에는 법적 근거가 필요하며 드라이버의 개인 정보 보호 고지에 공개되어야 합니다.
MapAtlas Routing API를 사용하여 추적 지도에 계획된 경로를 표시할 수 있습니까?
예. 이동이 생성될 때 Routing API에서 경로를 가져오고, 폴리라인을 디코딩하고, 지도에 GeoJSON 라인 레이어로 추가합니다. 드라이버가 이동하면 현재 위치에서 경로를 다시 가져와 동적으로 ETA를 재계산할 수 있습니다.
