GPX 파일에서 급경사와 급커브를 찾아주는 위험구간 분석기를 만들었다


라이딩 전에 무서운 구간을 먼저 보고 싶었다

GPX 파일을 보면 거리와 획득고도는 대략 알 수 있다.

하지만 실제로 코스를 타기 전에 궁금한 건
단순한 총거리만은 아니다.

어디가 급하게 올라가고,
어디가 길게 내려가고,
어느 지점에서 커브가 심하게 꺾이는가?

이런 정보는 숫자 하나로 잘 드러나지 않는다.

50km 코스라고 해도
평지 위주의 50km와
급경사 업힐, 긴 다운힐, 급커브가 섞인 50km는
완전히 다른 라이딩이다.

그래서 기존 GPX 난이도 계산기에 이어
새 도구를 하나 더 만들었다.

GPX 위험구간 분석기다.

한강 자전거길

이 도구가 찾아주는 것

GPX 파일을 선택하면 브라우저 안에서 파일을 읽고,
경로 포인트를 따라가며 위험 가능성이 있는 구간을 찾는다.

현재는 다음 유형을 탐지한다.

분석이 끝나면 전체 위험구간 수와
고위험, 주의, 참고 구간 개수를 요약해서 보여준다.

그리고 각 구간은 카드 목록으로 정리된다.

거리, 평균 경사, 최대 경사, 회전각 같은 정보를 보고
지도에서 해당 위치로 바로 이동할 수 있다.

카카오맵 API 키가 설정되어 있으면
지도 위에 전체 GPX 경로와 위험구간이 함께 표시된다.

개념만 놓고 보면 흐름은 단순하다.

const gpxText = await file.text();
const points = parseGpxText(gpxText);
const risks = detectGpxRisks(points);

map.renderRoute(points);
map.renderRisks(points, risks);

파일을 읽고,
GPX 포인트 배열로 바꾸고,
그 포인트를 한 칸씩 따라가며 위험구간 후보를 만든다.

여기서 중요한 것은 “지도에서 뭔가를 먼저 그리는 것”이 아니라
GPX 데이터를 숫자로 바꿔보는 것이다.

위도와 경도는 거리 계산에 쓰고,
고도는 경사도 계산에 쓰고,
연속된 세 점의 방향 변화는 커브 탐지에 쓴다.

지도와 로드뷰를 붙인 이유

경사도 숫자만 보는 것과
실제 길의 모양을 보는 것은 느낌이 다르다.

예를 들어 평균 경사 8%라는 숫자는
그 자체로도 의미가 있지만,
길이 좁은지, 커브가 있는지, 주변 시야가 열려 있는지는
숫자만으로 알기 어렵다.

그래서 위험구간 분석기에는 지도 이동과 로드뷰 보기를 붙였다.

위험구간 목록에서 지도 이동을 누르면
해당 구간 중심으로 지도가 이동한다.

로드뷰 보기를 누르면
가능한 경우 그 주변의 카카오 로드뷰를 불러온다.

로드뷰가 모든 지점에서 잡히는 것은 아니지만,
잡히는 구간에서는 라이딩 전에 길의 분위기를 보는 데 꽤 도움이 된다.

특히 다운힐과 급커브가 겹치는 구간은
지도만 볼 때보다 로드뷰로 한 번 확인하는 쪽이 훨씬 감이 좋다.

안양천과 학의천이 만나는 자전거길

파일은 서버로 올리지 않는다

이 도구도 기존 GPX 난이도 계산기와 마찬가지로
사용자가 선택한 GPX 파일을 서버로 업로드하지 않는다.

브라우저에서 File.text()로 파일을 읽고,
DOMParser로 GPX XML을 파싱한 뒤,
자바스크립트로 거리와 경사, 회전각을 계산한다.

집 근처 출발지나 출퇴근 동선이 들어 있는 GPX라면
외부 서비스에 업로드하는 게 조금 찜찜할 수 있다.

그래서 이 도구는
“내가 가진 GPX를 로컬에서 빠르게 확인한다”는 방향을 유지했다.

다만 지도와 로드뷰를 쓰려면
카카오맵 SDK는 브라우저에서 로드된다.

파일 자체를 보내는 것과
지도 타일이나 로드뷰를 불러오는 것은 다른 문제라서,
이 부분은 사용 목적에 맞게 판단하면 된다.

GPX를 파싱하는 부분도 특별한 서버 처리가 아니다.

브라우저가 이미 XML 파서를 가지고 있으므로
대략 이런 식으로 트랙 포인트를 꺼낼 수 있다.

function parseGpxText(gpxText) {
    const doc = new DOMParser().parseFromString(gpxText, 'application/xml');
    const trackPoints = [...doc.querySelectorAll('trkpt')];

    return trackPoints.map((node) => ({
        lat: Number(node.getAttribute('lat')),
        lng: Number(node.getAttribute('lon')),
        ele: Number(node.querySelector('ele')?.textContent),
    }));
}

실제 구현에서는 여기에 누적 거리, 고도 누락 처리,
잘못된 GPX 파일 검증 같은 처리가 더 붙는다.

하지만 기본 개념은 위 코드와 같다.

GPX는 결국 XML이고,
위험구간 분석은 그 XML에서 뽑은 좌표 배열을 계산하는 일이다.

탐지 기준은 일부러 단순하게 시작했다

위험구간 판정은 처음부터 너무 복잡하게 만들지 않았다.

현재 기준은 대략 이런 식이다.

경사도는 두 점 사이의 고도 차이를 거리로 나눠 계산한다.

function gradePercent(prev, next) {
    if (prev.ele == null || next.ele == null) return null;

    const distance = distanceMeters(prev, next);
    if (distance < 1) return null;

    return ((next.ele - prev.ele) / distance) * 100;
}

예를 들어 100m를 이동하는 동안 고도가 10m 올랐다면
경사도는 10%가 된다.

이 값이 연속해서 10% 이상 나오면
급경사 업힐 후보로 묶는다.

반대로 음수 값이 크게 나오면
다운힐 구간으로 본다.

급커브는 고도가 아니라 방향 변화를 본다.

function turnAngleDegrees(a, b, c) {
    const bearing1 = bearingDegrees(a, b);
    const bearing2 = bearingDegrees(b, c);

    let diff = Math.abs(bearing2 - bearing1);
    if (diff > 180) diff = 360 - diff;

    return diff;
}

세 점 a -> b -> c를 지나갈 때
진행 방향이 얼마나 바뀌는지 보는 방식이다.

이 각도가 일정 기준 이상이면
b 근처를 급커브 후보로 표시한다.

물론 실제 위험도는 훨씬 많은 요소에 영향을 받는다.

노면 상태, 차도와의 거리, 날씨, 시야, 라이더의 숙련도,
자전거 종류와 브레이크 상태까지 모두 영향을 준다.

그래서 이 도구가 말하는 위험구간은
절대적인 판정이라기보다는
“라이딩 전에 한 번 더 볼 만한 구간”에 가깝다.

목적은 겁을 주는 것이 아니라
미리 살펴볼 지점을 줄여주는 것이다.

구간을 묶는 방식도 단순하다.

조건에 맞는 포인트가 이어지는 동안 시작점을 기억해두고,
조건이 끊기는 순간 하나의 위험구간으로 저장한다.

let startIndex = null;
const grades = [];

for (let i = 1; i < points.length; i++) {
    const grade = gradePercent(points[i - 1], points[i]);
    const matched = grade != null && grade >= 10;

    if (matched) {
        if (startIndex == null) startIndex = i - 1;
        grades.push(grade);
        continue;
    }

    if (startIndex != null) {
        pushRiskSegment(startIndex, i - 1, grades);
        startIndex = null;
        grades.length = 0;
    }
}

이런 식으로 만들면
짧게 튀는 데이터 하나에 너무 예민하게 반응하지 않고,
“이어지는 구간”을 하나의 카드로 보여줄 수 있다.

언제 쓰면 좋을까

이 도구는 특히 처음 타보는 GPX 코스를 볼 때 유용하다.

예를 들어 이런 상황이다.

이미 익숙한 출퇴근길도 한 번 넣어보면 재미있다.

평소에 “여기는 왜 이렇게 힘들지?”라고 느꼈던 구간이
경사도나 긴 업힐로 잡히는 경우가 있다.

반대로 몸으로는 별로 어렵지 않았던 구간이
숫자로는 급경사로 잡힐 수도 있다.

그 차이를 보는 것도 꽤 흥미롭다.

다혼 대쉬 D16 라이딩 사진

수정사항

공개 후 샘플 GPX를 다시 넣어보니
일부 구간에서 고도 데이터가 튀면서
급경사나 급다운힐이 과하게 잡힐 수 있다는 점을 확인했다.

특히 정지에 가까운 구간이나
GPS 포인트가 아주 촘촘한 구간에서는
실제 길의 경사보다 고도 변화가 더 크게 보일 수 있다.

처음 구현은 인접한 두 포인트의 고도 차이로 경사를 계산했다.

const grade = gradePercent(points[i - 1], points[i]);

이 방식은 단순하고 빠르지만,
고도 데이터가 조금만 흔들려도
짧은 구간이 20% 이상 급경사처럼 보일 수 있다.

그래서 경사 탐지 기준을 조금 더 보수적으로 바꿨다.

이렇게 바꾸면 아주 짧은 GPS 흔들림이
바로 위험구간으로 표시되는 일을 줄일 수 있다.

물론 모든 오탐이 사라지는 것은 아니다.

실제 짧고 가파른 업힐이 있을 수도 있고,
기기마다 고도 데이터 품질이 다르기 때문이다.

그래도 이 도구의 목적이
“라이딩 전에 한 번 더 확인할 만한 구간을 줄여주는 것”이라면,
순간적인 수치보다 일정 거리 동안 이어지는 흐름을 보는 편이 더 낫다고 판단했다.

아직 더 다듬고 싶은 것들

지금 버전은 첫 공개 버전에 가깝다.

앞으로는 조금 더 실용적으로 다듬고 싶다.

결국 목표는 하나다.

라이딩 전에 GPX 파일을 열었을 때
“이 코스는 어디를 조심해야 하는지”
조금 더 빨리 감을 잡게 해주는 것.

코스를 완벽하게 예측할 수는 없지만,
미리 보는 것만으로도 라이딩은 꽤 달라진다.

특히 낯선 코스라면
출발 전에 5분 정도 투자해서
GPX 위험구간 분석기로 한 번 훑어보는 것을 추천한다.


관련 태그 글

각주