포트폴리오에 있는 '자세히 보기' 팝업만으로는 SafePath AI가 실제로 어떻게 '안전한' 경로를 찾아서 보여주는지를 온전히 전달하기가 아무래도 어려웠어요. 몇 줄짜리 요약으로는 왜 경로 하나를 추천하는 데 지도 데이터와 공공데이터, 그리고 알고리즘까지 함께 엮어야 했는지까지는 담을 수가 없더라고요. 그래서 이번 글에서는 팝업에 못다 적은 이야기까지 포함해서, SafePath AI의 기술 스택을 처음부터 끝까지 자세히 풀어보려고 해요.
전체 구조, 한눈에 보기
SafePath AI는 사용자가 출발지와 도착지를 입력하면, 단순히 최단 거리가 아니라 '이 길이 얼마나 안전한지'까지 함께 계산해서 경로를 추천해주는 서비스예요. 겉으로는 지도 위에 선 하나 그려주는 것처럼 단순해 보이지만, 그 뒤에서는 생각보다 여러 층의 데이터와 계산이 맞물려 돌아가고 있어요.
- 백엔드 — Python Flask로 만들어졌고, 화면 렌더링(템플릿)과 API 응답을 하나의 서버가 함께 처리해요.
- PWA 지원 —
manifest.json을 통해 홈 화면에 앱처럼 설치할 수 있도록 만들어 뒀어요. - 데이터 소스 — 정부에서 공개한 안전 관련 공공데이터(CSV)와, 그때그때 가져오는 외부 API(지오코딩, 보안등 정보, 도보 경로 탐색)를 함께 조합해서 써요.
- 캐시 레이어 — 지오코딩 결과, 히트맵 계산 결과, 경로 계산 결과까지 총 세 종류의 캐시를 파일 기반으로 두고 있어요. 같은 연산을 반복하거나 외부 API를 불필요하게 두드리는 일을 줄이기 위해서예요.
공공데이터, 있는 그대로 쓰지 않고 한 번 더 다듬어서 쓰기
서버가 켜질 때 미리 읽어두는 데이터
서버가 시작되는 순간, 비상벨 위치, 안심지킴이집, CCTV 위치 데이터를 CSV 파일에서 통째로 읽어 메모리에 올려둬요. 요청이 들어올 때마다 파일을 다시 여는 대신, 이미 메모리에 있는 값을 바로 꺼내 쓰기 위해서예요. 다만 공공데이터 CSV들이 저마다 인코딩이 달라서, cp949 · euc-kr · utf-8 순서로 여러 인코딩을 순차적으로 시도해가며 안전하게 읽어내도록 만들었어요.
주소만 있는 데이터는 좌표로 바꿔서
공중화장실 데이터처럼 위도·경도 없이 주소만 적혀 있는 데이터도 있었어요. 그래서 Nominatim(OpenStreetMap) 지오코딩 API를 붙여서 주소를 좌표로 변환하도록 했어요. 다만 외부 API를 매번 호출하면 속도도 느려지고 요청 제한에 걸릴 위험도 있어서, 한 번 변환한 주소는 geocode_cache.json에 저장해두고 다음부터는 캐시에서 바로 꺼내 쓰도록 만들었어요. 지오코딩 서버에 부담을 주지 않기 위해 호출 사이에 1초씩 간격을 두었고, 요청 한 번당 새로 지오코딩을 시도하는 횟수 자체에도 상한을 걸어뒀어요.
실시간으로 받아오는 보안등 정보
보안등 데이터는 CSV로 들고 있지 않고, 국가안전지도(safemap.go.kr)의 WFS(공간정보) API를 그때그때 호출해서 가져와요. 프론트엔드가 이 외부 API를 직접 두드리는 대신, 서버가 중간에서 프록시 역할을 하도록 만들어서 호출 방식과 응답 형태를 서버 쪽에서 일관되게 관리할 수 있게 했어요.
위험도 히트맵, 점이 아니라 면으로 보여주기
CCTV나 비상벨의 위치를 점으로만 콕콕 찍어서 보여주는 방식으로는 '이 동네가 전반적으로 얼마나 안전한지'를 한눈에 파악하기가 어려웠어요. 그래서 지도를 잘게 격자(grid)로 나누고, 각 칸마다 안전 점수를 계산해서 색으로 표현하는 히트맵을 만들었어요.
- 지도의 확대 단계(zoom)에 따라 격자 한 칸의 크기를 다르게 계산해요. 더 확대할수록 더 촘촘한 격자로 계산되도록 했어요.
- 비상벨·안심지킴이집·CCTV·보안등·화장실처럼 데이터 종류마다 안전에 기여하는 정도가 다르다고 보고 서로 다른 가중치를 줬어요. (비상벨 3점, CCTV·안심지킴이집 2점, 보안등·화장실 1점)
- 시설 하나가 있다고 그 칸만 안전해지는 건 아니라고 생각해서, 바로 옆 칸에는 55%, 대각선 칸에는 22%의 영향을 나눠주는 방식으로 주변 칸까지 자연스럽게 퍼지도록 만들었어요.
- 계산이 끝난 점수는 0~1 사이 값으로 정규화한 뒤, 빨강(위험)에서 노랑을 거쳐 초록(안전)으로 이어지는 색상으로 변환하고, 밀도가 높을수록 조금 더 진하게 보이도록 투명도도 함께 조절했어요.
- 한 번 계산한 격자는 bbox와 zoom 값을 키로 삼아 파일로 캐시해 둬서, 같은 영역을 다시 요청할 때는 재계산 없이 바로 응답할 수 있어요.
안전 경로 알고리즘, 최단 거리보다 중요하게 본 것
SafePath AI에서 가장 많은 시간을 쏟은 부분을 하나만 꼽으라면, 망설임 없이 이 경로 계산 로직을 꼽을 거예요. 그냥 최단 거리를 찾아주는 거라면 지도 API 하나로도 충분하지만, '안전한' 경로를 찾아주려면 거리와 위험도를 동시에 저울질하는 저만의 계산이 필요했거든요.
- 가능한 환경에서는 OSMnx + NetworkX로 실제 도로망을 그래프로 그린 뒤, 각 도로 구간(edge)마다 단순 거리가 아니라 '거리 + 위험 요소 − 안전 요소'로 재계산한 가중치를 부여하고, 이 가중치의 합이 가장 낮은 경로를 최단 경로 알고리즘으로 찾아요. 주변에 안전시설이 적은 구간일수록 가중치가 높아져서, 자연스럽게 그 구간을 피해 가도록 만든 셈이에요.
- 두 라이브러리를 쓸 수 없는 환경이라면, 공개 라우팅 서버인 OSRM에서 여러 개의 대안 경로를 받아온 뒤, 각 경로가 지나는 지점들의 위험도 평균을 계산해서 그중 가장 점수가 좋은 경로를 골라주는 방식으로 대체하도록 만들었어요.
- 매번 도로망을 새로 계산하는 건 비용이 크기 때문에, 출발지·도착지가 이전 요청과 30m 이내로 가까우면 다시 계산하지 않고 캐시된 경로를 그대로 재사용하도록 했어요.
- 완성된 경로는 구간별로 위험도를 다시 계산해서 빨강 · 주황 · 초록 세 단계로 나눠, 지도 위에서 어느 구간이 상대적으로 더 위험한지 색만 보고도 바로 알 수 있게 만들었어요.
무거운 연산을 지키는 최소한의 장치
경로 계산 한 번에 도로망 그래프 생성부터 히트맵 조회까지 서버 리소스가 꽤 들어가다 보니, 이 기능이 무분별하게 반복 호출되는 상황은 막아야 했어요. 그래서 경로 추천 API를 처리하기 전에 Google reCAPTCHA v3 검증을 먼저 거치도록 만들었고, 점수(score)가 0.5 미만이거나 의도한 액션(action)이 아니라고 판단되면 무거운 계산을 아예 시작하지도 않고 요청 자체를 차단하도록 했어요.
사용자가 직접 체감할 수 있는 UX · UI
지금까지는 눈에 보이지 않는 곳에서 벌어지는 이야기였다면, 이제부터는 실제로 사용자가 화면을 통해 마주하게 되는 기능들을 소개해 드릴게요.
① 로딩 상태를 숨기지 않고, 오히려 보여주기

지도 위에 안전시설 데이터를 올릴 때마다 사용자를 마냥 기다리게 만들고 싶지 않았어요. 그래서 데이터를 불러오는 동안에는 화면 한쪽에 작은 카드로 진행 상황을 보여주도록 만들었어요.
특히 이미 캐시에 있는 영역을 다시 요청했을 때는 "캐시에서 로드됨"이라는 문구와 함께 진행률이 곧바로 100%로 채워지는데, 서버 안에서만 조용히 동작하던 캐시 로직이 사용자 눈에도 '체감 속도'로 드러나는 순간이라 개인적으로도 마음에 드는 디테일이에요.
② 화면 크기에 맞춰 모양을 바꾸는 경로 탐색 창

PC 화면에서는 지도 왼쪽에 고정된 패널로 '경로 탐색' 창을 띄웠지만, 같은 구조를 모바일 화면에 그대로 옮기면 좁은 화면에서 지도 대부분을 가려버리는 문제가 있었어요. 그래서 모바일에서는 화면 아래에서 올라오는 바텀시트(bottom sheet) 형태로 바꾸고, 위쪽 손잡이를 드래그하거나 닫기 버튼을 눌러서 원할 때 접을 수 있게 만들었어요.
출발지·도착지 입력창 바로 아래에는 '내 위치' 버튼을 나란히 둬서, 주소를 직접 입력하지 않아도 탭 한 번으로 현재 위치를 지정할 수 있도록 했고요.
③ 방향 안내에 '조심해야 할 구간'까지 얹기

경로를 찾고 나면 지도 위에는 실제 이동 경로가 그려지고, 왼쪽 패널에는 전체 거리·평균 안전도·예상 소요 시간과 구간 수까지 한 번에 요약해서 보여줘요.
그 아래로는 실제 내비게이션처럼 "우회전 291m", "좌회전 1.4km" 같은 턴바이턴 안내가 이어지는데, 그냥 방향만 알려주는 게 아니라 구간마다 '조심해야 할 구간'이라는 라벨을 함께 달아서, 안전 점수가 낮게 계산된 구간을 지날 때는 사용자가 미리 마음의 준비를 할 수 있게 만들었어요.
지도 바탕에 깔린 위험도 히트맵과 실제로 그어지는 경로 선을 나란히 놓고 보면, 왜 최단 거리가 아니라 이 경로로 안내됐는지도 자연스럽게 이해가 돼요.