홈으로 돌아가기

iOS GPS 트래커: 칼만 필터와 지오해시 구현

SwiftUI에서 칼만 필터와 지오해시를 사용한 백그라운드 GPS 트래커 구현 기사. 데이터 처리, 경로 시각화 및 App Store 심사 통과를 위한 기술 솔루션 상세 분석.

iOS 백그라운드 GPS 트래커: 완전한 기술 가이드
Advertisement 728x90

# # iOS에서 백그라운드 GPS 트래커 만들기: 기술적 해결책과 함정

2024년에 Google Timeline이 종료된 후, 개발자들은 경로 분석을 위한 신뢰할 수 있는 도구를 잃었습니다. 클라우드 기반 솔루션 대신, 이제 우리는 연료 소비를 정확하게 추적하는 로컬 앱을 구축하고 있습니다. SwiftUI에서 GPS 트래커를 구현하는 과정을 안내하겠습니다. 노이즈 데이터 처리, 백그라운드 최적화, App Store 심사 통과 팁을 중점적으로 다루겠습니다.

CoreLocation에서의 백그라운드 추적 기본

주요 과제는 앱이 백그라운드 상태일 때 좌표를 획득하면서도 배터리를 과도하게 소모하지 않는 것입니다. 이를 위해 필요한 설정은 다음과 같습니다:

  • CLLocationManager에서 allowsBackgroundLocationUpdates 활성화
  • 위치 유형의 Background Modes 권한 추가
  • Info.plist에 NSLocationAlwaysAndWhenInUseUsageDescription으로 사용 사유 명시

초기 구현에서는 모든 포인트를 기록해 여행당 수천 개의 포인트가 쌓였습니다. 다음 매개변수를 조합해 최적화했습니다:

Google AdInline article slot
  • desiredAccuracy = kCLLocationAccuracyBest — Wi-Fi/셀룰러 기지국 대신 GPS 우선
  • distanceFilter = 10 — 10미터 미만 이동 무시
  • horizontalAccuracy > 50 필터링 — 노이즈 데이터 제거

이 설정으로 여행당 데이터가 200~500포인트로 줄었습니다. 60개 이상의 경로로 데이터베이스가 ~15 MB를 차지하는데, CoreData로 충분히 관리 가능합니다.

IMU 없이 Kalman 필터: 고속도로 추적을 위한 수학

GPS 신호 방해 구간(Krasnodar–Gelendzhik)에서는 고전적인 센서 기반 솔루션이 불가능합니다 — 백그라운드에서 CMMotionManager가 차단되기 때문입니다. 좌표만으로 Kalman 필터를 적응시켰습니다:

func processLocation(_ location: CLLocation) -> CLLocation {
    guard let previous = lastFilteredLocation else {
        lastFilteredLocation = location
        return location
    }

    let dt = location.timestamp.timeIntervalSince(previous.timestamp)
    let processNoise = dt * speedVariance
    predictedVariance += processNoise

    let measurementVariance = location.horizontalAccuracy * location.horizontalAccuracy
    let kalmanGain = predictedVariance / (predictedVariance + measurementVariance)

    let lat = previous.coordinate.latitude
        + kalmanGain * (location.coordinate.latitude - previous.coordinate.latitude)
    let lon = previous.coordinate.longitude
        + kalmanGain * (location.coordinate.longitude - previous.coordinate.longitude)

    predictedVariance = (1 - kalmanGain) * predictedVariance

    return CLLocation(
        coordinate: CLLocationCoordinate2D(latitude: lat, longitude: lon),
        altitude: location.altitude,
        horizontalAccuracy: sqrt(predictedVariance),
        verticalAccuracy: location.verticalAccuracy,
        timestamp: location.timestamp
    )
}

이 알고리즘은 측정값에 대한 신뢰도를 동적으로 조정합니다: horizontalAccuracy가 높을 때(신호 약함) kalmanGain이 낮아져 이전 데이터에 더 의존합니다. 10초 이상 신호 끊김 시에는 큐빅 스플라인 후처리를 사용합니다.

Google AdInline article slot

SwiftUI에서 속도별 경로 시각화

iOS 17+에서는 MapPolyline를 제공하지만, 객체당 한 색상으로 제한됩니다. 속도 기반 동적 색상화를 위해 다음을 구현했습니다:

  • 균일한 속도 구간으로 트랙 분할
  • 각 구간을 별도의 MapPolyline로 렌더링
  • 인접 구간을 한 포인트만큼 겹침
struct SpeedSegment {
    let coordinates: [CLLocationCoordinate2D]
    let color: Color
}

func buildSegments(from points: [LocationPoint]) -> [SpeedSegment] {
    var segments: [SpeedSegment] = []
    var currentCoords: [CLLocationCoordinate2D] = []
    var currentColor: Color = .green

    for point in points {
        let color = speedColor(for: point.speed)
        if color != currentColor && !currentCoords.isEmpty {
            segments.append(SpeedSegment(
                coordinates: currentCoords,
                color: currentColor
            ))
            currentCoords = [currentCoords.last!]
        }
        currentCoords.append(point.coordinate)
        currentColor = color
    }

    if !currentCoords.isEmpty {
        segments.append(SpeedSegment(
            coordinates: currentCoords,
            color: currentColor
        ))
    }
    return segments
}

시스템은 4개의 색상 구간을 사용합니다:

  • Green: < 50 km/h (도시)
  • Yellow: 50–90 km/h (고속도로)
  • Orange: 90–110 km/h (고속도로)
  • Red: > 110 km/h

일반적인 경로에서 20~50개의 폴리라인을 생성합니다. 500+ 포인트에서 구형 장치에 지연이 발생할 수 있지만, 대부분의 경우 성능이 안정적입니다.

Google AdInline article slot

Geohash Fog of War: 지리공간 최적화

미탐색 구간을 숨기는 "fog of war" 효과를 위해 quadtree나 직접 포인트 저장 대신 geohash를 선택했습니다. 장점:

  • 접두사 매칭으로 빠른 조회
  • CoreData로 간단한 직렬화
  • 정밀도 매개변수로 유연성

| 정밀도 | 셀 크기 | 용도 |

|--------|----------------|------------------|

| 5 | ~4.9 × 4.9 km | 국가 개요 |

| 6 | ~1.2 × 0.6 km | 도시 개요 |

| 7 | ~150 × 150 m | 상세 보기 |

구현 내용:

  • MKCoordinateSpan 기반 동적 정밀도 전환
  • Set<String>으로 가시 셀 확인
  • 불투명도 0.7s easeOut 애니메이션으로 소실 효과

현재 제한: 직사각형 셀 경계. 다음 단계: 가장자리 블러링으로 그라데이션 렌더링.

주요 요약

  • 입력 필터링이 핵심: distanceFilter와 horizontalAccuracy 확인 없이는 데이터베이스가 5~10배 부풀어 오름
  • Live Activity는 엄격한 제한: iOS가 허용하는 최대 1 Hz 업데이트
  • Geohash가 quadtree보다 우수 — 모바일에서 직렬화가 간단
  • IMU 없는 Kalman 필터로 10초 신호 끊김까지 처리
  • MapPolyline는 동적 색상을 위해 분할 필요 — 한 경로 = 20~50 객체

Live Activity: 인간 오류 방지

문제: 사용자가 녹음을 멈추는 걸 잊어 배터리 소모. 해결: ActivityKit으로 Lock Screen 표시기:

// Launch activity
let activity = try Activity.request(
    attributes: TripActivityAttributes(carName: "Polo Sedan"),
    content: .init(state: initialState)
)

// Update every second
await activity.update(.init(state: updatedState))

주요 포인트: 업데이트 주파수는 ~1 Hz로 제한. 더 자주 보내면 시스템이 무시합니다. 속도계에는 충분하지만, 고주파 지표는 데이터 집계가 필요합니다.

CoreData 데이터 저장

데이터베이스 스키마는 경로와 포인트에 대한 빠른 액세스를 위해 최적화되었습니다:

TripEntity (1) ──── (*) LocationPointEntity
    ├── startDate
    ├── distance
    └── fuelCost

LocationPointEntity
    ├── latitude
    ├── longitude
    └── speed

ExploredCellEntity
    └── geohash (unique)

구현 세부사항:

  • geohash 셀에 NSMergeByPropertyObjectTrumpMergePolicy
  • 타임스탬프 기반 오래된 기록 자동 삭제
  • tripID와 타임스탬프로 인덱싱해 쿼리 속도 향상

경로당 500+ 포인트로, fetch 요청 최소화가 중요합니다. 모든 LocationPointEntity 작업은 배치 업데이트와 함께 단일 관리 객체 컨텍스트에서 실행됩니다.

— Editorial Team

Advertisement 728x90

다음 읽기