실무에서 추적관리(관제) 프로그램을 개발하면서 지도 위에 자산/차량의 이동 경로를 안정적으로 표시하고, 반복적으로 경로를 계산해야 하는 요구사항이 생겼습니다. 이 과정에서 상용 길찾기 API의 비용 구조와 호출 제한, 운영 환경에서의 확장성 문제를 직접 마주했고, 그 대안으로 카카오맵 + OSRM 조합을 적용했습니다.

실제 추적관리 프로그램(위치/이동 이력 관제 성격의 시스템)을 개발하던 중, 지도 렌더링은 카카오맵으로 처리하면서 경로 계산은 비용 효율적으로 운영할 수 있는 구조가 필요했습니다. 단순 데모가 아니라 재조회, 실시간 갱신, 다회 렌더링, 운영 안정성까지 고려해야 했기 때문에 OSRM을 연동하는 방식으로 설계했고, 적용 후 구조가 안정적으로 정리되었습니다.
왜 카카오맵 + OSRM 조합을 선택했는가
위치 기반 서비스(LBS), 물류, 관제, 자산 추적 시스템에서는 출발지~목적지 경로 계산과 지도 위 경로 시각화가 핵심 기능입니다. 하지만 상용 길 찾기 API는 호출량이 늘어날수록 비용 부담이 커지고, 대규모 트래픽 환경에서는 비용 예측이 어려워질 수 있습니다.
실무에서 추적관리 프로그램을 개발하다 보면 단순 1회 경로 계산이 아니라, 경로 재계산, 다중 자산 모니터링, 반복 조회, 화면 갱신 등의 요구가 계속 발생합니다. 이런 환경에서는 지도 UI와 경로 계산 엔진을 분리하는 설계가 운영 비용과 유지보수 측면에서 매우 유리합니다.
카카오맵은 사용자에게 친숙한 국내 지도 UI/POI/오버레이 렌더링을 담당하고, OSRM은 경로 계산(거리/시간/최적 경로) 전담 엔진으로 사용합니다. 이렇게 분리하면 실무에서 비용, 성능, 확장성 모두 잡기 쉬워집니다.
카카오맵이 좋은 이유
- 국내 사용자에게 익숙한 지도 UX
- 마커/폴리라인/오버레이 렌더링 편의성
- 국내 서비스에 적합한 POI 표현
- 프론트엔드 구현 속도가 빠름
OSRM이 좋은 이유
- 오픈소스 기반으로 비용 부담 완화
- 고성능 라우팅 엔진 (대규모 도로망 대응)
- 자체 서버 운영 가능 (온프레미스/클라우드)
- 프로파일/Lua 커스터마이징 가능
가장 중요한 포인트 : 좌표 순서가 다르다
카카오맵과 OSRM을 연동할 때 가장 많이 실수하는 부분은 좌표 순서입니다. 이 부분만 정확히 이해해도 경로가 엉뚱한 위치에 찍히는 문제를 대부분 방지할 수 있습니다.
OSRM 입력/응답 좌표 : [경도, 위도] = [lng, lat]
카카오맵 LatLng 생성자 : new kakao.maps.LatLng(lat, lng)
즉, OSRM 좌표를 카카오맵에 넣기 전에 순서를 뒤집어야 합니다.
팀 내 공통 유틸 함수(예 : toKakaoLatLng(osrmCoord))를 만들어두면 좌표 순서 실수를 크게 줄일 수 있습니다.
OSRM Route API 요청 구조와 필수 옵션
일반적인 Route API 호출 구조는 아래와 같습니다. 실무에서는 overview와 geometries 선택이 특히 중요합니다.
GET http://{osrm-server}:{port}/route/v1/driving/{lng,lat;lng,lat}?overview=full&geometries=geojson
| 옵션 | 권장값 | 설명 |
|---|---|---|
| overview | full | 도로 굴곡까지 자연스럽게 폴리라인을 그리려면 full 권장 |
| geometries | geojson / polyline | 개발 편의성은 geojson, 네트워크 최적화는 polyline 유리 |
| steps | false (기본) | 턴바이턴 안내가 필요할 때만 true 사용 |
| alternatives | false (기본) | 대체 경로가 필요한 경우에만 true 설정 |
| annotations | 필요 시만 | 세그먼트별 거리/시간/속도 분석이 필요할 때 사용 |
GeoJSON vs Polyline 선택 기준 (실무 의사결정 포인트)
경로 geometry 응답은 geometries=geojson 또는 geometries=polyline 중 하나를 선택하게 됩니다. 프로젝트 단계(초기 개발/운영 확장)에 따라 선택 전략이 달라집니다.
GeoJSON 추천 상황
- PoC, 초기 구현, 빠른 검증
- 브라우저 파싱 직관성이 중요할 때
- 팀 내 온보딩 속도를 높이고 싶을 때
- 내부망/트래픽 부담이 상대적으로 낮을 때
Polyline 추천 상황
- 모바일/대규모 트래픽 운영
- 응답 페이로드 크기 최적화가 중요할 때
- OSRM 호출량이 많은 서비스
- 클라이언트 디코딩 비용을 감수할 수 있을 때
초기엔 GeoJSON으로 빠르게 구현하고, 운영 트래픽이 커지면 Polyline으로 전환하는 단계적 접근이 가장 현실적입니다.
카카오맵 + OSRM 연동 실전 예제 (GeoJSON 기준)
아래 예제는 카카오맵에 OSRM 경로를 폴리라인으로 그리는 기본 구조이면서, 실무에서 자주 필요한 재호출 시 이전 폴리라인 제거, AbortController로 요청 취소, setBounds 자동 화면 맞춤까지 포함한 형태입니다.
1) index.html
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kakao + OSRM Route Demo</title>
<style>
html, body { margin:0; padding:0; height:100%; }
#map { width:100%; height:100vh; }
.panel{
position:absolute; top:16px; left:16px; z-index:10;
background:rgba(255,255,255,.96); padding:14px;
border-radius:10px; box-shadow:0 8px 20px rgba(0,0,0,.12);
font-family:"Malgun Gothic", sans-serif;
width:min(92vw, 380px);
}
.panel h3{ margin:0 0 10px; font-size:16px; }
.panel button{
border:0; background:#FEE500; color:#222; font-weight:700;
border-radius:8px; padding:10px 12px; cursor:pointer;
}
.panel button:disabled{ opacity:.65; cursor:not-allowed; }
#meta{ margin-top:10px; font-size:13px; color:#444; line-height:1.5; }
</style>
</head>
<body>
<div class="panel">
<h3>OSRM 경로 탐색</h3>
<button id="btnRoute" type="button" onclick="calculateAndDrawRoute()">경로 계산 및 표시</button>
<p id="meta">버튼을 눌러 경로를 탐색하세요.</p>
</div>
<div id="map"></div>
<script src="//dapi.kakao.com/v2/maps/sdk.js?appkey=YOUR_KAKAO_APP_KEY"></script>
<script src="./osrm-kakao-integration.js"></script>
</body>
</html>
2) osrm-kakao-integration.js
let activePolyline = null;
let startMarker = null;
let endMarker = null;
let currentAbortController = null;
const map = new kakao.maps.Map(document.getElementById('map'), {
center : new kakao.maps.LatLng(37.382239, 127.121755),
level : 5
});
const metaEl = document.getElementById('meta');
const btnEl = document.getElementById('btnRoute');
async function calculateAndDrawRoute() {
// 예시 좌표 (판교역 -> 정자역 부근)
const origin = { lat : 37.394726, lng : 127.111158 };
const destination = { lat : 37.367317, lng : 127.108847 };
// 퍼블릭 OSRM 서버 (데모/테스트용)
// 실서비스에서는 자체 OSRM 서버 또는 백엔드 프록시 권장
const osrmBaseUrl = 'https://router.project-osrm.org/route/v1/driving/';
// OSRM 좌표 순서 : [lng, lat]
const coordinatesString = `${origin.lng},${origin.lat};${destination.lng},${destination.lat}`;
// 폴리라인 렌더링에 유리한 전체 경로 + GeoJSON
const queryParams = '?overview=full&geometries=geojson&alternatives=false&steps=false';
const requestUrl = osrmBaseUrl + coordinatesString + queryParams;
// 이전 요청 취소 (연속 클릭/갱신 시 레이스 컨디션 방지)
if (currentAbortController) currentAbortController.abort();
currentAbortController = new AbortController();
btnEl.disabled = true;
metaEl.innerText = '경로 계산 중입니다...';
try {
const res = await fetch(requestUrl, { signal : currentAbortController.signal });
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = await res.json();
if (data.code !== 'Ok' || !data.routes || data.routes.length === 0) {
throw new Error(`OSRM error : ${data.code || 'NoRoute'}`);
}
const route = data.routes[0];
const coords = route.geometry && route.geometry.coordinates ? route.geometry.coordinates : [];
if (coords.length === 0) throw new Error('Empty geometry');
const distanceKm = (route.distance / 1000).toFixed(2);
const durationMin = Math.round(route.duration / 60);
metaEl.innerText = `총 거리 : ${distanceKm}km / 예상 시간 : ${durationMin}분`;
const linePath = [];
const bounds = new kakao.maps.LatLngBounds();
// OSRM [lng, lat] -> Kakao LatLng(lat, lng)
coords.forEach(([lng, lat]) => {
const p = new kakao.maps.LatLng(lat, lng);
linePath.push(p);
bounds.extend(p);
});
// 기존 오버레이 정리 (누적 렌더링 방지)
if (activePolyline) activePolyline.setMap(null);
if (startMarker) startMarker.setMap(null);
if (endMarker) endMarker.setMap(null);
activePolyline = new kakao.maps.Polyline({
path : linePath,
strokeWeight : 6,
strokeColor : '#2563EB',
strokeOpacity : 0.9,
strokeStyle : 'solid',
endArrow : true
});
activePolyline.setMap(map);
startMarker = new kakao.maps.Marker({ position : linePath[0], map });
endMarker = new kakao.maps.Marker({ position : linePath[linePath.length - 1], map });
// 경로 전체가 보이도록 자동 포커싱
map.setBounds(bounds);
} catch (e) {
if (e.name === 'AbortError') return; // 이전 요청 취소는 정상 흐름
console.error(e);
metaEl.innerText = '오류가 발생했습니다. 콘솔 로그를 확인하세요.';
alert('OSRM 경로 탐색/렌더링 중 오류가 발생했습니다.');
} finally {
btnEl.disabled = false;
}
}
router.project-osrm.org는 퍼블릭 테스트 서버이므로 상업 서비스에 그대로 사용하는 것은 권장되지 않습니다. 실서비스는 자체 OSRM 서버 또는 백엔드 프록시(BFF) 구조를 권장합니다.
Docker로 자체 OSRM 서버 구축하기 (실서비스 전환용)
운영 단계에서는 OSRM 퍼블릭 서버 대신 자체 OSRM 백엔드를 구성하는 것이 안정적입니다. 아래는 대한민국 OSM PBF 데이터를 기준으로 한 기본 구축 예시입니다.
1) 대한민국 OSM 데이터 다운로드
wget http://download.geofabrik.de/asia/south-korea-latest.osm.pbf
2) OSRM 전처리 (MLD 파이프라인)
sudo docker run -t -v "${PWD}:/data" osrm/osrm-backend \
osrm-extract -p /opt/car.lua /data/south-korea-latest.osm.pbf
sudo docker run -t -v "${PWD}:/data" osrm/osrm-backend \
osrm-partition /data/south-korea-latest.osrm
sudo docker run -t -v "${PWD}:/data" osrm/osrm-backend \
osrm-customize /data/south-korea-latest.osrm
3) OSRM 라우팅 서버 실행
sudo docker run -d -p 5000:5000 --name osrm-routing-server \
-v "${PWD}:/data" osrm/osrm-backend \
osrm-routed --algorithm mld /data/south-korea-latest.osrm
브라우저에서 OSRM을 직접 호출하기보다 Frontend → Backend(BFF/Proxy) → OSRM 구조를 권장합니다. CORS, 인증, 로깅, 캐싱, 장애 대응, 요청 제한을 백엔드에서 통합 관리할 수 있기 때문입니다.
실무 최적화 체크리스트 (성능/안정성/유지보수)
- 좌표 변환 규칙 표준화: OSRM = [lng,lat], Kakao = (lat,lng) 문서화
- 오버레이 누적 방지: 폴리라인/마커 재렌더링 전에 setMap(null)
- 요청 제어: AbortController + debounce/throttle 적용
- 응답 최적화: 초기 GeoJSON, 운영 확장 시 Polyline 검토
- 캐싱 전략: 반복 경로는 백엔드 캐싱으로 OSRM 호출 절감
- 예외 처리: NoRoute/Timeout/HTTP 오류 코드별 사용자 메시지 분리
- 모니터링: OSRM 응답시간, 오류율, 메모리 사용량 지표 수집
확장 기능 : 단순 경로 표시를 넘어 실무 시스템으로 확장하기
추적관리 프로그램이나 물류/관제 시스템에서는 단순 A→B 경로 표시를 넘어서, 다양한 라우팅/분석 기능이 필요해집니다. OSRM은 아래 기능들로 확장이 가능합니다.
- table : 다대다 거리/시간 행렬 계산 (배차/순회 우선순위 계산)
- match : GPS 궤적 도로 스냅핑 (맵 매칭, 노이즈 보정)
- trip : 다중 목적지 방문 순서 최적화 (TSP 휴리스틱)
- Lua 프로파일 : 차량/이륜차/도보/특수 차량 규칙 커스터마이징
GPS 위치가 흔들리는 환경에서는 match 기반 맵 매칭이 실제 도로를 따라 움직인 것처럼 궤적 품질을 크게 개선해 줍니다.
참고 문서 (공식 문서/저장소)
아래 문서는 실제 구현 및 운영 단계에서 자주 확인하게 되는 공식 문서들입니다. 카카오맵 SDK 사용법, OSRM Route API 옵션, 자체 OSRM 서버 구축/운영 시 기본 레퍼런스로 활용하시면 좋습니다.
| 구분 | 문서명 | 설명 / 활용 포인트 |
|---|---|---|
| 카카오맵 | Kakao 지도 Web API (소개/시작 페이지) | 카카오맵 웹 API 소개, 사용 개요, 주요 기능 확인용 시작점. 앱 키 발급 후 빠르게 샘플을 확인할 때 유용합니다. |
| 카카오맵 | Kakao 지도 Web API 문서 (클래스/메서드) | kakao.maps.Map, Polyline, LatLng, LatLngBounds 등 실제 구현에 필요한 클래스 명세 확인용. |
| 카카오 개발자 | Kakao Developers 지도 API 시작하기 | 앱 등록, 플랫폼 설정(도메인 등록), 키 관리, 기본 적용 절차를 정리할 때 참고하기 좋습니다. |
| OSRM (공식) | OSRM API Documentation (v5.x) | route, table, match, trip 엔드포인트 및 overview, geometries, annotations 등 옵션 확인용 핵심 문서. |
| OSRM (공식 저장소) | Project-OSRM / osrm-backend (GitHub) | Docker 이미지 사용 예시, 빌드/실행 방식, 최신 변경사항 추적, 이슈/토론 확인에 유용합니다. |
| OSM 데이터 | Geofabrik - South Korea OSM Extract | 대한민국 OSM PBF 다운로드 페이지. 자체 OSRM 서버 구축 시 입력 데이터(.osm.pbf) 확보에 사용합니다. |
1) 카카오맵 Web API 문서로 Map / Polyline / Bounds 사용법 확인
2) OSRM API 문서로 route 옵션(overview, geometries) 정리
3) Geofabrik에서 OSM PBF 확보 후 자체 OSRM 구축
4) 운영 단계에서는 OSRM GitHub 저장소로 버전 변경/이슈 확인
OSRM 문서는 버전별 URL이 분리되어 있으므로, 운영 중인 OSRM 버전과 문서 버전을 맞춰 확인하는 것이 좋습니다. (예: v5.24.x 기준 문서 사용)
트러블슈팅 FAQ (카카오맵 + OSRM 연동 시 자주 겪는 문제)
경로가 바다/해외/엉뚱한 위치에 표시됩니다
경로를 여러 번 그리면 화면이 느려지고 잔상이 남습니다
CORS 오류가 발생합니다
GeoJSON과 Polyline 중 무엇부터 쓰는 게 좋을까요?
퍼블릭 OSRM 서버를 운영 서비스에 써도 되나요?
마무리
이번 정리는 단순한 이론 요약이 아니라, 실제로 추적관리(관제) 프로그램을 개발하면서 필요했던 기능을 구현하고 운영 관점까지 고려해 적용한 내용을 바탕으로 정리한 것입니다. 지도 UI는 카카오맵으로, 경로 연산은 OSRM으로 분리하는 구조는 비용/성능/확장성 측면에서 실무적으로 매우 현실적인 선택이었습니다.
특히 물류, 자산 추적, 차량 관제, 현장 운영 대시보드처럼 경로 호출이 빈번한 서비스에서는 초기에 아키텍처를 잘 잡아두는 것만으로도 이후 유지보수 난이도와 운영 비용이 크게 달라집니다. 핵심은 좌표 변환 규칙, 렌더링 객체 정리, 백엔드 프록시(BFF) + OSRM 운영 구조를 처음부터 명확히 설계하는 것입니다.
저와 비슷하게 실무에서 추적관리/지도 기반 기능을 개발하고 계신 분들이라면, 이 글의 구조와 예제 코드를 출발점으로 삼아도 충분히 도움이 될 것이라고 생각합니다. 시행착오를 줄이고 빠르게 안정화하는 데 분명히 도움이 되길 바랍니다.
추적관리/관제/물류/운영 대시보드처럼 지도 위에 이동 경로를 표시해야 하는 기능을 개발 중인데, 상용 길찾기 API 비용이나 호출 제한, 운영 확장성 때문에 대안을 찾고 있는 개발자
좌표 순서 변환 규칙, setBounds 자동 포커싱, 재렌더링 시 오버레이 정리, OSRM 자체 구축 또는 프록시 구조, GeoJSON/Polyline 선택 전략까지 정리하면 시행착오를 크게 줄일 수 있습니다.