🇺🇸 EN

📍 Core Location 완전정복

GPS 위치 추적부터 지오펜싱까지. 위치 기반 서비스의 모든 것!

⭐ 난이도: ⭐⭐ ⏱️ 예상 시간: 1-2h 📂 App Services

✨ Core Location이란?

Core Location은 GPS, Wi-Fi, 셀룰러를 활용해 사용자 위치를 제공합니다. 위치 추적, 지오펜싱, 나침반 등을 구현할 수 있습니다.

🔐 권한 요청

Info.plist
<!-- 필수: 사용 목적 설명 -->

<!-- 앱 사용 중에만 위치 -->
<key>NSLocationWhenInUseUsageDescription</key>
<string>현재 위치를 지도에 표시하기 위해 필요합니다</string>

<!-- 항상 위치 (백그라운드) -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>지오펜싱 알림을 위해 백그라운드 위치 접근이 필요합니다</string>

📍 현재 위치 가져오기

LocationManager.swift
import CoreLocation

class LocationManager: NSObject, CLLocationManagerDelegate, ObservableObject {
    private let manager = CLLocationManager()

    @Published var location: CLLocation?
    @Published var authorizationStatus: CLAuthorizationStatus?

    override init() {
        super.init()
        manager.delegate = self
        manager.desiredAccuracy = kCLLocationAccuracyBest
    }

    // 권한 요청
    func requestPermission() {
        manager.requestWhenInUseAuthorization()
        // 또는 manager.requestAlwaysAuthorization()
    }

    // 위치 추적 시작
    func startUpdating() {
        manager.startUpdatingLocation()
    }

    // 위치 추적 중지
    func stopUpdating() {
        manager.stopUpdatingLocation()
    }

    // 한 번만 위치 가져오기 (iOS 14+)
    func requestLocation() {
        manager.requestLocation()
    }

    // Delegate - 위치 업데이트
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        location = locations.last
        print("위치: \(location!.coordinate.latitude), \(location!.coordinate.longitude)")
    }

    // Delegate - 권한 변경
    func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
        authorizationStatus = manager.authorizationStatus

        switch manager.authorizationStatus {
        case .authorizedWhenInUse, .authorizedAlways:
            startUpdating()
        case .denied, .restricted:
            print("위치 권한 거부")
        case .notDetermined:
            requestPermission()
        @unknown default:
            break
        }
    }

    // Delegate - 에러
    func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
        print("위치 오류: \(error.localizedDescription)")
    }
}

📱 SwiftUI 통합

LocationView.swift
import SwiftUI

struct LocationView: View {
    @StateObject private var locationManager = LocationManager()

    var body: some View {
        VStack(spacing: 20) {
            if let location = locationManager.location {
                Text("📍 현재 위치")
                    .font(.headline)

                Text("위도: \(location.coordinate.latitude)")
                Text("경도: \(location.coordinate.longitude)")
                Text("고도: \(location.altitude)m")
                Text("정확도: \(location.horizontalAccuracy)m")
            } else {
                Text("위치 정보 없음")

                Button("위치 권한 요청") {
                    locationManager.requestPermission()
                }
                .buttonStyle(.borderedProminent)
            }
        }
        .padding()
    }
}

🎯 지오펜싱 (Geofencing)

Geofencing.swift
import CoreLocation

class GeofenceManager: NSObject, CLLocationManagerDelegate {
    private let manager = CLLocationManager()

    override init() {
        super.init()
        manager.delegate = self
    }

    // 지오펜스 추가 (특정 지역 모니터링)
    func startMonitoring(
        identifier: String,
        center: CLLocationCoordinate2D,
        radius: CLLocationDistance
    ) {
        // 권한 체크
        guard manager.authorizationStatus == .authorizedAlways else {
            manager.requestAlwaysAuthorization()
            return
        }

        // 원형 지역 생성
        let region = CLCircularRegion(
            center: center,
            radius: radius,  // 미터 단위 (최대 400m)
            identifier: identifier
        )

        region.notifyOnEntry = true  // 진입 시 알림
        region.notifyOnExit = true  // 퇴출 시 알림

        manager.startMonitoring(for: region)
    }

    // 지오펜스 제거
    func stopMonitoring(identifier: String) {
        for region in manager.monitoredRegions {
            if region.identifier == identifier {
                manager.stopMonitoring(for: region)
            }
        }
    }

    // Delegate - 지역 진입
    func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
        print("✅ \(region.identifier) 지역에 진입했습니다")
        // 로컬 알림 표시
        sendNotification(title: "지역 진입", body: "\(region.identifier)에 도착했습니다")
    }

    // Delegate - 지역 퇴출
    func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
        print("🚪 \(region.identifier) 지역을 벗어났습니다")
        sendNotification(title: "지역 퇴출", body: "\(region.identifier)을 떠났습니다")
    }

    func sendNotification(title: String, body: String) {
        // UNUserNotificationCenter 사용
    }
}

// 사용 예시
let geofence = GeofenceManager()
geofence.startMonitoring(
    identifier: "집",
    center: CLLocationCoordinate2D(latitude: 37.5665, longitude: 126.9780),
    radius: 100  // 100미터
)

🧭 나침반 (Heading)

Compass.swift
import CoreLocation

class CompassManager: NSObject, CLLocationManagerDelegate, ObservableObject {
    private let manager = CLLocationManager()

    @Published var heading: Double = 0  // 0-360도

    override init() {
        super.init()
        manager.delegate = self
        manager.headingFilter = 5  // 5도 이상 변경 시만 업데이트
    }

    func startUpdatingHeading() {
        if CLLocationManager.headingAvailable() {
            manager.startUpdatingHeading()
        }
    }

    func stopUpdatingHeading() {
        manager.stopUpdatingHeading()
    }

    // Delegate - 방향 업데이트
    func locationManager(_ manager: CLLocationManager, didUpdateHeading newHeading: CLHeading) {
        heading = newHeading.trueHeading  // 진북 기준
        // newHeading.magneticHeading - 자북 기준

        print("방향: \(heading)° \(getDirection(heading))")
    }

    func getDirection(_ heading: Double) -> String {
        switch heading {
        case 0..<22.5: return "북"
        case 22.5..<67.5: return "북동"
        case 67.5..<112.5: return "동"
        case 112.5..<157.5: return "남동"
        case 157.5..<202.5: return "남"
        case 202.5..<247.5: return "남서"
        case 247.5..<292.5: return "서"
        case 292.5..<337.5: return "북서"
        default: return "북"
        }
    }
}

📏 거리 계산

Distance.swift
import CoreLocation

// 두 좌표 사이 거리 (미터)
func calculateDistance(from: CLLocation, to: CLLocation) -> CLLocationDistance {
    return from.distance(from: to)
}

// 사용 예시
let seoul = CLLocation(latitude: 37.5665, longitude: 126.9780)
let busan = CLLocation(latitude: 35.1796, longitude: 129.0756)

let distance = calculateDistance(from: seoul, to: busan)
print("거리: \(distance / 1000)km")  // 약 325km

⚙️ 정확도 설정

Accuracy.swift
let manager = CLLocationManager()

// 정확도 설정 (배터리 소모 ↔️ 정확도 트레이드오프)
manager.desiredAccuracy = kCLLocationAccuracyBest  // 최고 정확도
manager.desiredAccuracy = kCLLocationAccuracyNearestTenMeters  // 10m
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters  // 100m
manager.desiredAccuracy = kCLLocationAccuracyKilometer  // 1km
manager.desiredAccuracy = kCLLocationAccuracyThreeKilometers  // 3km

// 최소 이동 거리 설정 (이보다 적게 이동하면 업데이트 안 함)
manager.distanceFilter = 10  // 10미터

// 배터리 절약 모드 (iOS 9+)
manager.allowsBackgroundLocationUpdates = false  // 백그라운드 업데이트 끄기

🔋 백그라운드 위치 추적

BackgroundLocation.swift
// 1. Info.plist에 추가
// NSLocationAlwaysAndWhenInUseUsageDescription

// 2. Capabilities 설정
// Xcode > Signing & Capabilities > Background Modes
// ✅ Location updates 체크

// 3. 코드 설정
let manager = CLLocationManager()
manager.allowsBackgroundLocationUpdates = true
manager.pausesLocationUpdatesAutomatically = false

// Always 권한 요청
manager.requestAlwaysAuthorization()

// ⚠️ 주의: 백그라운드 위치는 배터리를 많이 소모합니다!
// 꼭 필요한 경우에만 사용하세요.

📊 위치 정확도 확인

AccuracyCheck.swift
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    guard let location = locations.last else { return }

    // 수평 정확도 (작을수록 정확)
    print("수평 정확도: \(location.horizontalAccuracy)m")

    // 수직 정확도 (고도)
    print("수직 정확도: \(location.verticalAccuracy)m")

    // 정확도가 낮으면 무시
    if location.horizontalAccuracy > 100 {
        print("정확도 너무 낮음, 무시")
        return
    }

    // 오래된 데이터 무시 (5초 이상)
    if Date().timeIntervalSince(location.timestamp) > 5 {
        print("오래된 위치 데이터, 무시")
        return
    }

    // 유효한 위치 데이터 사용
    print("✅ 정확한 위치: \(location.coordinate)")
}

⚠️ 배터리 주의!
- 위치 추적은 배터리를 많이 소모합니다
- 적절한 정확도와 distanceFilter 설정
- 불필요할 때는 stopUpdatingLocation() 호출
- 백그라운드 위치는 꼭 필요할 때만

💡 HIG 체크리스트
✅ 위치 사용 목적 명확히 설명
✅ 적절한 권한만 요청 (WhenInUse vs Always)
✅ 권한 거부 시 앱 정상 작동
✅ 배터리 소모 최소화
✅ 정확도 트레이드오프 고려

📦 학습 자료

💻
GitHub 프로젝트
🍎
Apple HIG 원문
📖
Apple 공식 문서

📎 Apple 공식 자료

📘 공식 문서 💻 샘플 코드 🎬 WWDC 세션