[SmoothRide] Mapbox를 통한 시각화

저번에 folium을 통해서 시각화하는 건 가능했다. 하지만 용량이 너무 크고, 렉이 많이 걸리는 문제가 발생했다. 서울만 하는 것을 목표로 했는데도 이러면, 택도 없다.
또한 저번에 한 것은 api를 실시간으로 받아오는 것이 아닌, 노드와 링크 매칭인데도 그렇다.

그래서 이번에는 실시간으로 api를 받아오는 것과 함께, 이를 웹페이지에 시각화하기로 했다. 무엇을 사용할지 고민하다가, 웹페이지에서 지도를 띄울 떄는 leatflet이나 mapbox를 사용한다는 것을 알게 되었다.

처음 들어봐서 무슨 차이지 하고 지피티에게 물어보니..

✅ 3. Mapbox GL JS
Mapbox는 OSM 기반의 고급 지도 라이브러리예요.

장점

WebGL 기반으로 렌더링 성능이 우수함 (대량 데이터 처리에 강함)
3D 지도 및 실시간 데이터 시각화에 유리
다양한 지도 스타일 및 레이어 지원
실시간 GPS 트래킹과 연동이 쉬움
단점

비용 발생: 무료 계정 제한이 있으며, API 요청량이 많으면 유료 플랜 필요
Leaflet보다 라이브러리가 무거움
구현 난이도: 🔥🔥🔥 (Leaflet보다 복잡하지만, 기능이 훨씬 강력함)
✅ 2. Leaflet
Leaflet은 OSM 타일을 쉽게 활용할 수 있는 JavaScript 라이브러리예요.
즉, Leaflet을 사용하면 OSM 데이터를 쉽게 웹에서 렌더링할 수 있어요.

장점

경량(39KB)으로 속도가 빠르고 가볍게 사용 가능
반응형으로 웹사이트에 쉽게 적용 가능
무료 & 오픈소스로 상업적 사용 가능
다양한 플러그인 지원 (실시간 데이터, 클러스터링, 마커 등 추가 가능)
단점

3D 기능이 부족 (Google Maps API나 Mapbox GL JS에 비해)
내장된 고급 기능 부족 (예: 거리 측정, 고도 분석 등은 추가 구현 필요)
실시간 데이터 스트리밍을 지원하지만 별도 백엔드 구성 필요 (ex. WebSocket, MQTT 활용)
구현 난이도: 🔥🔥 (OSM보다 쉽지만, 실시간 데이터 연동을 위해 추가적인 백엔드 구현 필요)

이라고 했다. 단순 웹 지도가 필요했다면 leatflet을 사용했겠지만, 공공데이터를 통해 많은 데이터가 들어오는 프로젝트의 특성상, mapbox를 사용하는것이 더 낫다고 생각해 mapbox를 사용해보기로 했다.

FastAPI를 활용한 교통 데이터 API 구축

FastAPI를 사용해 교통 데이터를 받아오는 과정은 다음과 같다

  1. ITS API에서 도로 데이터를 가져와 가공

  2. 노드 ID를 위경도로 변환하여 GeoJSON 데이터 생성

  3. 실시간 데이터를 빠르게 처리할 수 있도록 Pandas로 최적화

    GeoJSON은 위경도 좌표를 포함하는 지리 공간 데이터를 표현하는 JSON 형식이다. 이구조를 통해 위치 정보를 저장할 수 있고, 지도 시각화 및 공간 데이터 처리가 가능하다.

    기본적으로

{
   "type": "FeatureCollection",
   "features": [
       {
           "type": "Feature",
           "geometry": {
               "type": "Point",
               "coordinates": [126.9784, 37.5662]
           },
           "properties": {
               "name": "서울 시청"
           }
       }
   ]
}

꼴로 생겼다. featurecollection은 여러 features를 포함한다는 형태이며,
features는 각 객체,
geometry는 Point, LineString, Polygon 등 해당하는 정보의 유형
coordinates는 패스
properties는 name, speed 같은 것들이다.

우리는

형태를 표현해야 하기 때문에.

def get_traffic_data():
    features = []
    for item in data["body"]["items"]:
        link_id = str(item["linkId"])
        if link_id in LINK_INFO:
            start_id = LINK_INFO[link_id]["start_node"]
            end_id = LINK_INFO[link_id]["end_node"]
            if start_id in NODE_COORDINATES and end_id in NODE_COORDINATES:
                start_coords = NODE_COORDINATES[start_id]
                end_coords = NODE_COORDINATES[end_id]
                features.append({
                    "type": "Feature",
                    "geometry": {
                        "type": "LineString",
                        "coordinates": [start_coords, end_coords]
                    },
                    "properties": {
                        "roadName": LINK_INFO[link_id]["road_name"],
                        "speed": float(item["speed"]),
                        "maxSpeed": LINK_INFO[link_id]["max_speed"]
                    }
                })
    return {"type": "FeatureCollection", "features": features}```

으로 했다. 한 결과는

{"type":"FeatureCollection","features":[{"type":"Feature","geometry":{"type":"LineString","coordinates":[[126.9793147853,37.5702029488],[126.9810231164,37.5702165858]]},"properties":{"roadName":"종로","speed":29.0,"maxSpeed":50,"travelTime":18.73,"length":150.925973393118}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[126.9810231164,37.5702165858],[126.9822109961,37.5701758602]]},"properties":{"roadName":"종로","speed":14.0,"maxSpeed":50,"travelTime":26.97,"length":104.866247215251}},{"type":"Feature","geometry":{"type":"LineString","coordinates":[[126.9822109961,37.5701758602],[126.9829758373,37.5702264188]]},"properties":{"roadName":"종로

꼴이다. 잘 나온다.

map.html

나는 프런트가 아니라 일단 html을 생으로 지피티와 만들었다. 돌아가는게 우선이기 때문에

<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="utf-8">
    <title>실시간 교통 지도</title>
    <script src="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.js"></script>
    <link href="https://api.mapbox.com/mapbox-gl-js/v2.10.0/mapbox-gl.css" rel="stylesheet">
    <style>
        body { margin: 0; padding: 0; }
        #map { width: 100vw; height: 100vh; }
    </style>
</head>
<body>
    <div id="map"></div>
    <script>
        // 1. Mapbox API Key 설정
        mapboxgl.accessToken = "pk.eyJ1IjoiaG9qb29ob2pvbyIsImEiOiJjbTdmbG1hdHIwMHUxMmlweWl1Y3RwbGhhIn0.yrlctaom6KuNafNdzj0INw"; 

        // 2. 지도 생성
        var map = new mapboxgl.Map({
            container: "map",
            style: "mapbox://styles/mapbox/streets-v11",
            center: [126.9780, 37.5665], // 서울 중심 좌표
            zoom: 12
        });

        async function loadTrafficData() {
            let configResponse = await fetch("/config");
            let config = await configResponse.json();
            let awsUrl = config.aws_url;
            let response = await fetch(`${awsUrl}/traffic`); // FastAPI에서 GeoJSON 가져오기
            let geojsonData = await response.json();

            if (map.getSource("traffic")) {
                map.getSource("traffic").setData(geojsonData);
            } else {
                map.addSource("traffic", { type: "geojson", data: geojsonData });

                // 3. 선(LineString) 추가 (도로 연결)
                map.addLayer({
                    id: "traffic-lines",
                    type: "line",
                    source: "traffic",
                    paint: {
                        "line-width": 5,
                        "line-color": [
                            "case",
                            ["<", ["get", "speed"], 15], "#FF0000",  // 속도 15km/h 미만 → 빨간색
                            ["<", ["get", "speed"], 25], "#FFA500",  // 속도 15~25km/h → 주황색
                            "#00FF00" // 속도 25km/h 이상 → 초록색
                        ],
                        "line-opacity": 0.8
                    }
                });

                // 4. 도로 이름 라벨 추가
                map.addLayer({
                    id: "traffic-labels",
                    type: "symbol",
                    source: "traffic",
                    layout: {
                        "symbol-placement": "line",
                        "text-field": ["get", "roadName"],
                        "text-font": ["Open Sans Bold", "Arial Unicode MS Bold"],
                        "text-size": 12,
                        "text-offset": [0, -1]
                    },
                    paint: {
                        "text-color": "#000000",
                        "text-halo-color": "#FFFFFF",
                        "text-halo-width": 1.5
                    }
                });
            }
        }

        // 5. 지도 로드 후 데이터 불러오기
        map.on("load", loadTrafficData);

        // 6. 10초마다 실시간 데이터 갱신
        setInterval(loadTrafficData, 10000);
    </script>
</body>
</html>

이를 통해 출력할 수 있다.

ec2에 올리기

SA에서 공부한 기억을 살려 해본다. 우선 루트 계정을 그대로 사용하면 안되기 때문에 IAM에서 SmoothRide-1 계정을 Admin으로 만들어준다.
그러고 보안 그룹에서 우선 모든 tcp 허용으로 인바운드를 설정해주면? 잘 돌아간다.

우 하 하

트러블슈팅

트러블슈팅까진 아니지만.. 처음해봐서 많이 헤맸다.
첫번째로는 로컬에서 실행하다보니 html에서 traffic 받아오는 주소 부분을 127.0.0.1로 해서 헤맸고,
python -m uvicorn main:app --reload 만 했다가 접속이 안되서 한참 헤맸다. 알고보니 --host 0.0.0.0을 붙여야 접속할 수 있다고 한다.
또 cd 잘못 눌렀다가 리눅스 맨 위까지 이동해서 파일 경로도 다시 공부하기도 했다.

마지막으로 로깅의 중요성을 다시 한번 깨달았는데, 로컬에서 아무생각없이 print를 하다보면 실제 서버에서 돌릴 때는 안보여서 망한다. 꼭 print가 아니라 로그로 표현하자.

말나온 김에

# 🔥 로깅 설정 (파일 + 콘솔)
logging.basicConfig(
    level=logging.INFO,  # 로그 레벨 설정
    format="%(asctime)s - %(levelname)s - %(message)s",  # 로그 출력 형식
    handlers=[
        logging.FileHandler("app.log", encoding="utf-8"),  # 로그를 파일로 저장
        logging.StreamHandler()  # 콘솔에도 출력
    ]
)

logger = logging.getLogger("fastapi_app")

이렇게 로깅을 설정한다.
로깅에는 Gunicorn을 사용하는 방법, 지금처럼 fastapi에 내장되어있는 logging을 사용하는 방법 등 여러가지가 있는데, 이건 더 확인해 볼 예정이다.

최종 요약

🚀 현재까지:

FastAPI에서 ITS API 데이터를 가져와 실시간 도로 시각화 완료
Mapbox에서 도로 정체 상태를 색상으로 표현하는 기능 구축
🔥 앞으로 해야 할 일:
✅ 실시간 데이터 처리 성능 개선 (WebSocket / Kafka 적용)
✅ 최적 경로 탐색 알고리즘 추가 (Dijkstra / A*)
✅ AI 기반 교통량 예측 기능 추가 (LSTM 활용)
✅ 클라우드 인프라 구축 (AWS Lambda, DynamoDB, S3 활용)

천천히 해보자.

0개의 댓글