ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • App Battery Drain
    iOS 2025. 8. 9. 09:44

    안녕하세요. 그린입니다 🍏
    이번 포스팅은 iOS 앱의 배터리 드레인 문제를 해결하는 방법에 대해 알아보겠습니다 🙋🏻


    iOS Battery Drain Solutions Deep Dive

    이런 경험 있으신가요?

    앱을 출시한 후 앱스토어 리뷰나 사용자 피드백에서,

    • "이 앱 쓰고 나서 배터리가 금방 없어져요 😡"
    • "백그라운드에서 계속 뭔가 하고 있는 것 같아요"
    • "핸드폰이 뜨거워져요"

    또는 iOS 설정 → 배터리에서 여러분의 앱이 배터리 사용량 상위권에 올라가 있는 상황 말이에요 😱
    이런 문제들은 단순히 사용자 불편함을 넘어서서 앱 삭제와 직결되는 심각한 이슈입니다.
    특히 iOS 12부터 배터리 사용량 화면이 더 상세해지면서 사용자들이 "범인" 앱을 쉽게 찾을 수 있게 되었거든요.


    Why Battery Matters More Than Ever?

    📱 사용자 행동 변화

    최신 연구에 따르면,

    • 배터리 드레인이 심한 앱은 24시간 내 삭제율이 3배 증가
    • 사용자의 76%가 배터리 사용량을 정기적으로 확인
    • "배터리 최적화" 관련 앱스토어 리뷰가 급증하는 추세

    ⚡ iOS의 배터리 보호 메커니즘

    Apple이 점점 강화하고 있는 정책들

    • Background App Refresh 자동 제한
    • Low Power Mode에서 백그라운드 작업 중단
    • 배터리 임계점에서 앱 자동 종료
    • iOS 15+에서는 사용자에게 배터리 드레인 앱 알림

    Energy Impact 측정의 현실 (Xcode 15 기준)

    🔍 Xcode Debug Navigator의 한계

    // ⌘+6으로 Debug Navigator를 열어도...
    // Energy Impact 게이지는 다음과 같은 문제들이 있어요:
    
    // 1. 때때로 아무것도 표시되지 않음
    // 2. 항상 0으로 표시되는 경우가 많음  
    // 3. 실제 기기에서만 동작 (시뮬레이터 X)
    // 4. 게임이나 무거운 앱에서는 부정확함

     

    그런데, 많은 개발자들이 Xcode 13 이후로 에너지 게이지가 제대로 작동하지 않는다고 보고하고 있어요.
    Apple이 이 기능들을 단계적으로 축소하면서 다른 방법을 권장하고 있는 상황입니다.


    🔍 사라진 도구들 (알아두면 좋아요)

    # ❌ Xcode 13부터 제거된 것들:
    # - Instruments의 "Energy Log" 템플릿
    # - iOS 15+ 기기의 설정 → 개발자 → 로깅 옵션
    # - 안정적인 Energy Impact 게이지 표시
    
    # 💡 Apple의 공식 입장:
    # "Xcode Organizer의 Metrics를 대신 사용하세요"
    # 하지만 이것도 앱스토어 배포 + 수천 명의 사용자가 필요함 😭

    🔍 현재 Xcode 15에서 실제로 사용 가능한 방법들

    방법 1 - 실제 기기 배터리 설정 확인 (가장 신뢰할 만한 방법)

    // iOS 기기에서:
    // 설정 → 배터리 → 지난 24시간/10일간 배터리 사용량
    // 여기서 여러분 앱의 실제 배터리 사용량을 확인할 수 있어요
    
    // 이 방법의 장점:
    // ✅ 실제 사용자 환경과 동일
    // ✅ 정확한 배터리 소모량 측정
    // ✅ 백그라운드 vs 포그라운드 사용량 구분

     
    방법 2 - Instruments의 다른 템플릿들 활용

    // Energy 전용 템플릿은 없지만, 간접적으로 측정 가능:
    
    // 1. "Activity Monitor" 템플릿
    //    - CPU 사용량
    //    - 메모리 사용량  
    //    - 네트워크 활동
    //    - 디스크 I/O
    
    // 2. "Time Profiler" 템플릿
    //    - CPU 집약적인 함수들 식별
    //    - 불필요한 연산 찾기
    
    // 3. "Network" 템플릿
    //    - 네트워크 요청 패턴 분석
    //    - 비효율적인 통신 찾기

     
    방법 3 - 수동 배터리 레벨 모니터링

    import UIKit
    
    class ManualBatteryMonitor {
        private var initialBatteryLevel: Float = 0
        private var testStartTime: Date = Date()
    
        func startBatteryTest() {
            UIDevice.current.isBatteryMonitoringEnabled = true
            initialBatteryLevel = UIDevice.current.batteryLevel
            testStartTime = Date()
    
            print("🔋 테스트 시작 - 배터리: \(Int(initialBatteryLevel * 100))%")
        }
    
        func endBatteryTest() {
            let finalBatteryLevel = UIDevice.current.batteryLevel
            let testDuration = Date().timeIntervalSince(testStartTime)
            let batteryDrain = initialBatteryLevel - finalBatteryLevel
    
            print("""
            🔋 테스트 결과:
            - 시작: \(Int(initialBatteryLevel * 100))%
            - 종료: \(Int(finalBatteryLevel * 100))%
            - 소모: \(Int(batteryDrain * 100))%
            - 시간: \(Int(testDuration))초
            - 시간당 소모율: \(Int(batteryDrain * 3600 / testDuration * 100))%/h
            """)
        }
    }
    
    // 사용법:
    let monitor = ManualBatteryMonitor()
    monitor.startBatteryTest()
    // 앱의 특정 기능 테스트...
    monitor.endBatteryTest()

    🔍 현재 가장 효과적인 측정 방법

    결론적으로, Xcode 15에서는 MetricKit + 실제 기기 배터리 설정 + 수동 모니터링 조합이 가장 신뢰할 만해요.

    // 1단계: 실제 기기 배터리 사용량 확인 (기준점)
    // 2단계: MetricKit으로 상세 데이터 수집  
    // 3단계: 수동 배터리 레벨 모니터링으로 검증
    // 4단계: Instruments 다른 템플릿으로 원인 분석

     

     


    MetricKit으로 실전 모니터링하기

    iOS 13부터 도입된 MetricKit은 배터리 문제 해결의 게임체인저예요!

     

    🚀 MetricKit 기본 설정

    import MetricKit
    
    class AppDelegate: UIResponder, UIApplicationDelegate {
    
        func application(_ application: UIApplication, 
                         didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    
            // MetricKit 구독 시작
            MXMetricManager.shared.add(self)
            return true
        }
    
        func applicationWillTerminate(_ application: UIApplication) {
            // 구독 해제
            MXMetricManager.shared.remove(self)
        }
    }
    
    // MARK: - MetricKit Subscriber
    extension AppDelegate: MXMetricManagerSubscriber {
    
        // 성능 메트릭 수신 (24시간마다)
        func didReceive(_ payloads: [MXMetricPayload]) {
            for payload in payloads {
                handleBatteryMetrics(payload)
            }
        }
    
        // 진단 데이터 수신 (iOS 14+에서는 즉시)
        @available(iOS 14.0, *)
        func didReceive(_ payloads: [MXDiagnosticPayload]) {
            for payload in payloads {
                handleCrashDiagnostics(payload)
            }
        }
    }

     


    🚀 배터리 메트릭 분석

    private func handleBatteryMetrics(_ payload: MXMetricPayload) {
        // 1. CPU 사용량 분석
        if let cpuMetrics = payload.cpuMetrics {
            let cumulativeCPUTime = cpuMetrics.cumulativeCPUTime
            print("💻 총 CPU 사용 시간: \(cumulativeCPUTime)")
            
            // CPU 시간이 너무 높으면 배터리 소모가 크다는 신호
            let cpuSeconds = cumulativeCPUTime.converted(to: .seconds).value
            if cpuSeconds > 3600 { // 1시간 이상
                print("⚠️ CPU 사용 시간이 너무 높음!")
            }
        }
        
        // 2. GPU 사용량 분석 (iOS 14+)
        if #available(iOS 14.0, *) {
            if let gpuMetrics = payload.gpuMetrics {
                let cumulativeGPUTime = gpuMetrics.cumulativeGPUTime
                print("🎮 총 GPU 사용 시간: \(cumulativeGPUTime)")
            }
        }
    
        // 3. 네트워크 사용량 분석
        // JSON 형태로 데이터를 확인해야 함
        if let networkMetrics = payload.networkTransferMetrics {
            // JSON으로 변환하여 실제 데이터 확인
            let jsonDict = networkMetrics.dictionaryRepresentation()
            print("📡 네트워크 메트릭: \(jsonDict)")
            
            // 또는 전체 payload를 JSON으로 출력
            if let jsonData = try? JSONSerialization.data(
                withJSONObject: payload.dictionaryRepresentation(),
                options: .prettyPrinted
            ),
               let jsonString = String(data: jsonData, encoding: .utf8) {
                print("📊 전체 메트릭 JSON:\n\(jsonString)")
            }
        }
    
        // 4. 위치 서비스 사용량
        if let locationMetrics = payload.locationActivityMetrics {
            // 실제 프로퍼티들:
            
            // Best accuracy (가장 정확한 위치, 배터리 소모 높음)
            let bestAccuracy = locationMetrics.cumulativeBestAccuracyForNavigationTime
            print("📍 Best Accuracy 시간: \(bestAccuracy)")
            
            // 10m accuracy
            if #available(iOS 14.0, *) {
                let nearestTenMeters = locationMetrics.cumulativeBestAccuracyTime
                print("📍 10m Accuracy 시간: \(nearestTenMeters)")
            }
            
            // 100m accuracy
            let hundredMeters = locationMetrics.cumulativeNearestTenMetersAccuracyTime
            print("📍 100m Accuracy 시간: \(hundredMeters)")
            
            // 1km accuracy
            let kilometer = locationMetrics.cumulativeHundredMetersAccuracyTime
            print("📍 1km Accuracy 시간: \(kilometer)")
            
            // 3km accuracy
            let threeKilometers = locationMetrics.cumulativeKilometerAccuracyTime
            print("📍 3km Accuracy 시간: \(threeKilometers)")
        }
        
        // 5. 디스플레이 메트릭 (iOS 14+)
        if #available(iOS 14.0, *) {
            if let displayMetrics = payload.displayMetrics {
                let averagePixelLuminance = displayMetrics.averagePixelLuminance
                print("📱 평균 화면 밝기: \(averagePixelLuminance)")
            }
        }
        
        // 6. 메모리 사용량
        if let memoryMetrics = payload.memoryMetrics {
            let peakMemory = memoryMetrics.peakMemoryUsage
            let averageMemory = memoryMetrics.averageSuspendedMemory
            print("💾 최대 메모리: \(peakMemory)")
            print("💾 평균 일시중단 메모리: \(averageMemory)")
        }
        
        // 7. 디스크 I/O (iOS 13+)
        if let diskMetrics = payload.diskIOMetrics {
            let cumulativeLogicalWrites = diskMetrics.cumulativeLogicalWrites
            print("💿 누적 디스크 쓰기: \(cumulativeLogicalWrites)")
        }
        
        // 8. 앱 실행 시간
        if let runtimeMetrics = payload.applicationTimeMetrics {
            let foregroundTime = runtimeMetrics.cumulativeForegroundTime
            let backgroundTime = runtimeMetrics.cumulativeBackgroundTime
            print("⏱ Foreground 시간: \(foregroundTime)")
            print("⏱ Background 시간: \(backgroundTime)")
            
            // 백그라운드 시간이 너무 길면 배터리 소모 증가
            let bgSeconds = backgroundTime.converted(to: .seconds).value
            if bgSeconds > 7200 { // 2시간 이상
                print("⚠️ 백그라운드 실행 시간이 너무 김!")
            }
        }
    }

     


    🚀 커스텀 메트릭 측정

    특정 기능의 에너지 사용량을 정확히 측정하고 싶다면

    import os.signpost
    
    class NetworkManager {
        private let signposter = OSSignposter(subsystem: "com.yourapp.network", category: "requests")
    
        func performAPICall() {
            // 메트릭 측정 시작
            let signpostID = signposter.makeSignpostID()
            let state = signposter.beginInterval("api_call", id: signpostID)
    
            URLSession.shared.dataTask(with: url) { data, response, error in
                // API 호출 완료
    
                // 메트릭 측정 종료
                self.signposter.endInterval("api_call", state)
    
                // 결과 처리...
            }.resume()
        }
    }
    
    // MetricKit에서 이 커스텀 메트릭을 받아보기
    private func handleCustomMetrics(_ payload: MXMetricPayload) {
        if let signpostMetrics = payload.signpostMetrics {
            for metric in signpostMetrics {
                if metric.signpostName == "api_call" {
                    print("🌐 API 호출 메트릭: \(metric)")
                    // CPU 시간, 메모리 사용량, 디스크 쓰기 등 자동 수집
                }
            }
        }
    }

     


    백그라운드 작업 최적화 전략

    배터리 드레인의 주범은 대부분 백그라운드 작업이에요.

     

    🎯 Background App Refresh 올바른 사용법

    import BackgroundTasks
    
    class BackgroundTaskManager {
        // OperationQueue 프로퍼티 추가 필요
        private let operationQueue = OperationQueue()
        
        func registerBackgroundTasks() {
            // 1. 가벼운 데이터 새로고침용
            BGTaskScheduler.shared.register(
                forTaskWithIdentifier: "com.yourapp.refresh",
                using: nil
            ) { task in
                self.handleAppRefresh(task: task as! BGAppRefreshTask)
            }
    
            // 2. 더 무거운 처리 작업용
            BGTaskScheduler.shared.register(
                forTaskWithIdentifier: "com.yourapp.processing",
                using: nil
            ) { task in
                self.handleBackgroundProcessing(task: task as! BGProcessingTask)
            }
        }
    
        private func handleAppRefresh(task: BGAppRefreshTask) {
            // ⚡ 핵심: 30초 이내 완료해야 함
            
            // 다음 작업 스케줄링 (먼저 해야 함)
            scheduleAppRefresh()
            
            let operation = createLightweightRefreshOperation()
    
            task.expirationHandler = {
                operation.cancel()
            }
    
            operation.completionBlock = {
                task.setTaskCompleted(success: !operation.isCancelled)
            }
    
            // 가벼운 작업만 수행
            // ✅ 좋은 예: 새 메시지 개수 확인, 날씨 데이터 업데이트
            // ❌ 나쁜 예: 대용량 파일 다운로드, 복잡한 계산
            operationQueue.addOperation(operation)
        }
        
        private func handleBackgroundProcessing(task: BGProcessingTask) {
            // 백그라운드 처리 작업 구현
            scheduleBackgroundProcessing()
            
            let operation = createProcessingOperation()
            
            task.expirationHandler = {
                operation.cancel()
            }
            
            operation.completionBlock = {
                task.setTaskCompleted(success: !operation.isCancelled)
            }
            
            operationQueue.addOperation(operation)
        }
    
        private func scheduleAppRefresh() {
            let request = BGAppRefreshTaskRequest(identifier: "com.yourapp.refresh")
            // 최소 15분 간격으로 설정
            request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60)
    
            do {
                try BGTaskScheduler.shared.submit(request)
            } catch {
                print("배경 작업 스케줄링 실패: \(error)")
            }
        }
        
        private func scheduleBackgroundProcessing() {
            let request = BGProcessingTaskRequest(identifier: "com.yourapp.processing")
            request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 60) // 1시간 후
            request.requiresNetworkConnectivity = true // 필요시 설정
            request.requiresExternalPower = false // 필요시 설정
            
            do {
                try BGTaskScheduler.shared.submit(request)
            } catch {
                print("백그라운드 처리 스케줄링 실패: \(error)")
            }
        }
        
        // 실제 구현 필요
        private func createLightweightRefreshOperation() -> Operation {
            return BlockOperation {
                // 가벼운 작업 수행
                print("Refresh 작업 수행 중...")
                // 예: API 호출, 데이터 업데이트 등
            }
        }
        
        private func createProcessingOperation() -> Operation {
            return BlockOperation {
                // 무거운 작업 수행
                print("Processing 작업 수행 중...")
                // 예: 데이터 동기화, 파일 처리 등
            }
        }
    }

     


    🎯 네트워크 요청 최적화

    class OptimizedNetworkManager {
    
        func createOptimalURLSession() -> URLSession {
            let config = URLSessionConfiguration.default
    
            // 1. 불필요한 요청 방지
            config.requestCachePolicy = .reloadRevalidatingCacheData
            config.urlCache = URLCache(memoryCapacity: 20 * 1024 * 1024, // 20MB
                                       diskCapacity: 100 * 1024 * 1024)   // 100MB
    
            // 2. 연결 풀링 활용
            config.httpMaximumConnectionsPerHost = 4
            config.httpShouldUsePipelining = true
    
            // 3. 타임아웃 최적화
            config.timeoutIntervalForRequest = 30
            config.timeoutIntervalForResource = 60
    
            // 4. 배터리 절약 모드 (WiFi만 사용)
            if ProcessInfo.processInfo.isLowPowerModeEnabled {
                config.allowsCellularAccess = false
            }
    
            return URLSession(configuration: config)
        }
    
        // 5. 요청 배칭 (Batching)
        func batchNetworkRequests() {
            // ❌ 나쁜 방법: 개별 요청
            // fetchUserProfile()
            // fetchUserPosts()  
            // fetchUserFriends()
    
            // ✅ 좋은 방법: 한 번에 모든 데이터 요청
            fetchUserDataBatch() { [weak self] result in
                switch result {
                case .success(let batchedData):
                    // 한 번에 모든 데이터 처리
                    self?.processBatchedData(batchedData)
                case .failure(let error):
                    print("배치 요청 실패: \(error)")
                }
            }
        }
    }

     


    🎯 위치 서비스 최적화

    위치 서비스는 배터리 드레인의 최대 주범 중 하나에요.

    import CoreLocation
    
    class OptimizedLocationManager: NSObject, CLLocationManagerDelegate {
        private let locationManager = CLLocationManager()
    
        override init() {
            super.init()
            setupLocationManager()
        }
    
        private func setupLocationManager() {
            locationManager.delegate = self
    
            // 1. 정확도 최적화
            // ✅ 좋음: 일반적인 용도
            locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
    
            // ❌ 피해야 할 설정: GPS 상시 사용
            // locationManager.desiredAccuracy = kCLLocationAccuracyBest
    
            // 2. 거리 필터 설정
            locationManager.distanceFilter = 100 // 100m 이상 이동 시에만 업데이트
    
            // 3. 배터리 절약 모드
            if #available(iOS 14.0, *) {
                locationManager.desiredAccuracy = kCLLocationAccuracyReduced
            }
        }
    
        func startLocationUpdates() {
            // 4. 사용 목적에 맞는 권한 요청
            switch locationManager.authorizationStatus {
            case .notDetermined:
                // 앱 사용 중에만 필요한 경우
                locationManager.requestWhenInUseAuthorization()
    
                // 정말 필요한 경우에만 Always 권한
                // locationManager.requestAlwaysAuthorization()
    
            case .authorizedWhenInUse, .authorizedAlways:
                locationManager.startUpdatingLocation()
    
            default:
                break
            }
        }
    
        // 5. 적절한 시점에 위치 추적 중단
        func stopLocationUpdates() {
            locationManager.stopUpdatingLocation()
        }
    
        // MARK: - CLLocationManagerDelegate
        func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
            guard let location = locations.last else { return }
    
            // 6. 필요한 정확도 달성 시 즉시 중단
            if location.horizontalAccuracy < 100 {
                stopLocationUpdates()
                // 위치 정보 처리...
            }
        }
    }

    최적화 사례들

    💡 사례 1- 메모리 누수로 인한 배터리 드레인

    // 🚫 문제가 되는 코드
    class LeakyViewController: UIViewController {
        private var timer: Timer?
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // 순환 참조 발생!
            timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                self.updateUI() // strong reference cycle!
            }
        }
    
        // deinit이 호출되지 않아서 timer가 계속 실행 → 배터리 드레인
    }
    
    // ✅ 해결된 코드
    class OptimizedViewController: UIViewController {
        private var timer: Timer?
    
        override func viewDidLoad() {
            super.viewDidLoad()
    
            // weak self로 순환 참조 방지
            timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
                self?.updateUI()
            }
        }
    
        override func viewDidDisappear(_ animated: Bool) {
            super.viewDidDisappear(animated)
    
            // 화면이 사라질 때 timer 정리
            timer?.invalidate()
            timer = nil
        }
    
        deinit {
            print("✅ ViewController가 제대로 해제됨")
        }
    }

     


    💡 사례 2 - 무분별한 백그라운드 네트워크 요청

    // 🚫 문제: 3초마다 배달 상태 확인
    class ProblematicDeliveryTracker {
        private var statusTimer: Timer?
    
        func startTracking() {
            statusTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { _ in
                self.fetchDeliveryStatus() // 배터리 킬러!
            }
        }
    }
    
    // ✅ 해결: 적응형 폴링 + 푸시 알림 조합
    class OptimizedDeliveryTracker {
        private var pollingInterval: TimeInterval = 30.0 // 30초부터 시작
        private var statusTimer: Timer?
    
        func startTracking() {
            // 1. 푸시 알림을 통한 실시간 업데이트 우선
            registerForPushNotifications()
    
            // 2. 폴링은 보조적으로만 사용
            startAdaptivePolling()
        }
    
        private func startAdaptivePolling() {
            statusTimer = Timer.scheduledTimer(withTimeInterval: pollingInterval, repeats: true) { _ in
                self.fetchDeliveryStatus { status in
                    // 배달 상태에 따라 폴링 간격 조정
                    switch status {
                    case .preparing:
                        self.pollingInterval = 60.0 // 1분
                    case .onTheWay:
                        self.pollingInterval = 15.0 // 15초
                    case .delivered:
                        self.stopTracking() // 완료 시 중단
                    }
    
                    self.rescheduleTimer()
                }
            }
        }
    
        private func rescheduleTimer() {
            statusTimer?.invalidate()
            startAdaptivePolling()
        }
    }

     


    💡 사례 3 - 이미지 처리 최적화

    // 🚫 문제: 메인 스레드에서 이미지 처리
    class ProblematicImageProcessor {
        func loadAndProcessImages(_ imageURLs: [URL]) {
            for url in imageURLs {
                // 메인 스레드 블로킹 + 높은 CPU 사용률
                let image = UIImage(contentsOfFile: url.path)
                let processedImage = applyFilters(to: image)
                displayImage(processedImage)
            }
        }
    }
    
    // ✅ 해결: 백그라운드 처리 + 배칭 + 캐싱
    class OptimizedImageProcessor {
        private let processingQueue = DispatchQueue(label: "image.processing", 
                                                   qos: .utility)
        private let imageCache = NSCache<NSString, UIImage>()
    
        func loadAndProcessImages(_ imageURLs: [URL]) {
            // 배치 크기로 나누어 처리
            let batchSize = 3
            let batches = imageURLs.chunked(into: batchSize)
    
            for batch in batches {
                processingQueue.async { [weak self] in
                    self?.processBatch(batch)
    
                    // 배치 간 잠시 휴식 (배터리 절약)
                    Thread.sleep(forTimeInterval: 0.1)
                }
            }
        }
    
        private func processBatch(_ urls: [URL]) {
            for url in urls {
                let cacheKey = url.absoluteString as NSString
    
                // 캐시 확인
                if let cachedImage = imageCache.object(forKey: cacheKey) {
                    DispatchQueue.main.async {
                        self.displayImage(cachedImage)
                    }
                    continue
                }
    
                // 이미지 로드 및 처리
                autoreleasepool {
                    guard let image = UIImage(contentsOfFile: url.path) else { return }
                    let processedImage = applyFilters(to: image)
    
                    // 캐시 저장
                    imageCache.setObject(processedImage, forKey: cacheKey)
    
                    DispatchQueue.main.async {
                        self.displayImage(processedImage)
                    }
                }
            }
        }
    }

    최적화 기법들

    🚀 시스템 상태 인식 코딩

    import UIKit
    
    class SmartBatteryManager {
    
        func adjustBehaviorBasedOnSystemState() {
            // 1. Low Power Mode 감지
            if ProcessInfo.processInfo.isLowPowerModeEnabled {
                enablePowerSavingMode()
            }
    
            // 2. 배터리 레벨 모니터링
            UIDevice.current.isBatteryMonitoringEnabled = true
            let batteryLevel = UIDevice.current.batteryLevel
    
            if batteryLevel < 0.2 { // 20% 미만
                enableAggressivePowerSaving()
            }
    
            // 3. 배터리 상태 변화 감지
            NotificationCenter.default.addObserver(
                self,
                selector: #selector(batteryStateChanged),
                name: UIDevice.batteryStateDidChangeNotification,
                object: nil
            )
        }
    
        @objc private func batteryStateChanged() {
            switch UIDevice.current.batteryState {
            case .charging, .full:
                enableNormalMode()
            case .unplugged:
                if UIDevice.current.batteryLevel < 0.3 {
                    enablePowerSavingMode()
                }
            default:
                break
            }
        }
    
        private func enablePowerSavingMode() {
            // 애니메이션 감소
            UIView.setAnimationsEnabled(false)
    
            // 네트워크 요청 간격 증가
            NetworkManager.shared.setPowerSavingMode(true)
    
            // 백그라운드 작업 최소화
            BackgroundTaskManager.shared.reducePowerConsumption()
        }
    }

     


    🚀 Core Data 배터리 최적화

    import CoreData
    
    class EfficientCoreDataManager {
    
        lazy var persistentContainer: NSPersistentContainer = {
            let container = NSPersistentContainer(name: "DataModel")
    
            // 배터리 최적화를 위한 설정
            let storeDescription = container.persistentStoreDescriptions.first
            storeDescription?.shouldMigrateStoreAutomatically = true
            storeDescription?.shouldInferMappingModelAutomatically = true
    
            // 메모리 맵 사용 비활성화 (배터리 절약)
            storeDescription?.setValue("DELETE" as NSString, 
                                      forKey: "journal_mode")
    
            container.loadPersistentStores { _, error in
                if let error = error {
                    print("CoreData 로드 실패: \(error)")
                }
            }
    
            return container
        }()
    
        func saveContextEfficiently() {
            let context = persistentContainer.viewContext
    
            // 변경사항이 없으면 저장하지 않음
            guard context.hasChanges else { return }
    
            do {
                try context.save()
            } catch {
                print("저장 실패: \(error)")
            }
    
            // 메모리 정리 (중요!)
            context.refreshAllObjects()
        }
    
        func performBackgroundTask<T>(_ block: @escaping (NSManagedObjectContext) -> T) {
            persistentContainer.performBackgroundTask { context in
                // 배치 크기 제한으로 메모리 사용량 조절
                context.stalenessInterval = 0.0
    
                let result = block(context)
    
                // 작업 완료 후 메모리 해제
                context.reset()
            }
        }
    }

    측정 및 모니터링 자동화

    📊 CI/CD에서 배터리 영향 체크

    #!/bin/bash
    # battery_check.sh
    
    echo "🔋 배터리 영향 자동 체크 시작"
    
    # 1. Xcode에서 Energy Impact 로그 추출
    xcodebuild test \
        -project YourApp.xcodeproj \
        -scheme YourApp \
        -destination 'platform=iOS,name=iPhone 15' \
        -resultBundlePath TestResults.xcresult
    
    # 2. 에너지 사용량 임계값 체크
    ENERGY_THRESHOLD=15
    CURRENT_ENERGY=$(xcrun xcresulttool get --path TestResults.xcresult --format json | jq '.metrics.energyImpact.value')
    
    if (( $(echo "$CURRENT_ENERGY > $ENERGY_THRESHOLD" | bc -l) )); then
        echo "⚠️ 에너지 사용량이 임계값을 초과했습니다!"
        echo "현재: $CURRENT_ENERGY, 임계값: $ENERGY_THRESHOLD"
        exit 1
    fi
    
    echo "✅ 배터리 영향 체크 통과"

    📊 Slack으로 배터리 메트릭 알림

    import MetricKit
    
    class BatteryMetricsReporter {
      private func sendBatteryReportToSlack(_ payload: MXMetricPayload) {
        var message = "🔋 **배터리 관련 메트릭 리포트**\n\n"
        
        // 1. CPU 사용량 (배터리 소모의 주요 지표)
        if let cpuMetrics = payload.cpuMetrics {
          let cpuTime = cpuMetrics.cumulativeCPUTime.converted(to: .seconds).value
          message += "💻 총 CPU 시간: \(String(format: "%.2f", cpuTime))초\n"
          
          if cpuTime > 3600 {
            message += "⚠️ CPU 사용량 높음!\n"
          }
        }
        
        // 2. GPU 사용량 (iOS 14+)
        if #available(iOS 14.0, *) {
          if let gpuMetrics = payload.gpuMetrics {
            let gpuTime = gpuMetrics.cumulativeGPUTime.converted(to: .seconds).value
            message += "🎮 총 GPU 시간: \(String(format: "%.2f", gpuTime))초\n"
          }
        }
        
        // 3. 앱 실행 시간
        if let runtimeMetrics = payload.applicationTimeMetrics {
          let foregroundTime = runtimeMetrics.cumulativeForegroundTime.converted(to: .seconds).value
          let backgroundTime = runtimeMetrics.cumulativeBackgroundTime.converted(to: .seconds).value
          
          message += "📱 포그라운드: \(String(format: "%.2f", foregroundTime))초\n"
          message += "🌙 백그라운드: \(String(format: "%.2f", backgroundTime))초\n"
          
          if backgroundTime > 7200 {
            message += "⚠️ 백그라운드 실행 시간이 너무 김!\n"
          }
        }
        
        // 4. 위치 서비스 사용 (배터리 소모 큼)
        if let locationMetrics = payload.locationActivityMetrics {
          let bestAccuracy = locationMetrics.cumulativeBestAccuracyForNavigationTime.converted(to: .seconds).value
          
          if bestAccuracy > 0 {
            message += "📍 고정밀 위치 사용: \(String(format: "%.2f", bestAccuracy))초\n"
            message += "⚠️ 고정밀 위치 서비스 사용 중!\n"
          }
        }
        
        // 5. 디스플레이 밝기 (iOS 14+)
        if #available(iOS 14.0, *) {
          if let displayMetrics = payload.displayMetrics {
            let brightness = displayMetrics.averagePixelLuminance?.averageMeasurement.value
            message += "📱 평균 화면 밝기: \(String(format: "%.2f", brightness)) cd/m²\n"
          }
        }
        
        // 6. 네트워크 사용 (간접 지표)
        if let networkMetrics = payload.networkTransferMetrics {
          let networkDict = networkMetrics.dictionaryRepresentation()
          message += "📡 네트워크 활동: \(networkDict.keys.count)개 항목\n"
        }
        
        // 7. 메모리 사용량
        if let memoryMetrics = payload.memoryMetrics {
          let peakMemory = memoryMetrics.peakMemoryUsage.converted(to: .megabytes).value
          message += "💾 최대 메모리: \(String(format: "%.2f", peakMemory))MB\n"
        }
        
        // 8. 전체 평가
        message += "\n📊 **종합 평가**\n"
        message += evaluateBatteryImpact(payload)
        
        // Slack Webhook으로 전송
        sendToSlack(message: message)
      }
      
      // 배터리 영향도 평가
      private func evaluateBatteryImpact(_ payload: MXMetricPayload) -> String {
        var issues: [String] = []
        
        // CPU 체크
        if let cpuMetrics = payload.cpuMetrics {
          let cpuTime = cpuMetrics.cumulativeCPUTime.converted(to: .seconds).value
          if cpuTime > 3600 {
            issues.append("높은 CPU 사용량")
          }
        }
        
        // 백그라운드 시간 체크
        if let runtimeMetrics = payload.applicationTimeMetrics {
          let backgroundTime = runtimeMetrics.cumulativeBackgroundTime.converted(to: .seconds).value
          if backgroundTime > 7200 {
            issues.append("과도한 백그라운드 실행")
          }
        }
        
        // 위치 서비스 체크
        if let locationMetrics = payload.locationActivityMetrics {
          let bestAccuracy = locationMetrics.cumulativeBestAccuracyForNavigationTime.converted(to: .seconds).value
          if bestAccuracy > 600 { // 10분 이상
            issues.append("고정밀 위치 서비스 과다 사용")
          }
        }
        
        if issues.isEmpty {
          return "✅ 정상 범위"
        } else {
          return "⚠️ 문제 발견:\n" + issues.map { "  • \($0)" }.joined(separator: "\n")
        }
      }
      
      private func sendToSlack(message: String) {
        guard let webhookURL = URL(string: "YOUR_SLACK_WEBHOOK_URL") else {
          print("❌ 유효하지 않은 Slack Webhook URL")
          return
        }
        
        var request = URLRequest(url: webhookURL)
        request.httpMethod = "POST"
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        
        let slackPayload = ["text": message]
        
        guard let httpBody = try? JSONSerialization.data(withJSONObject: slackPayload) else {
          print("❌ JSON 직렬화 실패")
          return
        }
        
        request.httpBody = httpBody
        
        URLSession.shared.dataTask(with: request) { data, response, error in
          if let error = error {
            print("❌ Slack 전송 실패: \(error.localizedDescription)")
            return
          }
          
          if let httpResponse = response as? HTTPURLResponse {
            if httpResponse.statusCode == 200 {
              print("✅ Slack 전송 성공")
            } else {
              print("⚠️ Slack 응답 코드: \(httpResponse.statusCode)")
            }
          }
        }.resume()
      }
    }

    Troubleshooting & 주의사항

    ⚠️ 흔한 실수들

    // 🚫 실수 1: 무한 타이머
    class BadExample {
        private var timer: Timer?
    
        func startTimer() {
            timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                // 중단 조건 없이 계속 실행 → 배터리 드레인
                self.doSomething()
            }
            // timer를 멈추는 코드가 없음!
        }
    }
    
    // ✅ 올바른 방법
    class GoodExample {
        private var timer: Timer?
        private var timerStartTime = Date()
    
        func startTimer() {
            timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
                guard let self = self else { return }
    
                // 시간 제한 두기 (예: 5분)
                if Date().timeIntervalSince(self.timerStartTime) > 300 {
                    self.stopTimer()
                    return
                }
    
                self.doSomething()
            }
        }
    
        func stopTimer() {
            timer?.invalidate()
            timer = nil
        }
    }

     


    ⚠️ MetricKit 사용 시 주의점

    // 🚫 실수: 모든 메트릭을 서버로 전송
    func badMetricsHandling(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            // 전체 페이로드를 그대로 전송 → 사용자 데이터 낭비
            sendToServer(payload)
        }
    }
    
    // ✅ 올바른 방법: 필요한 데이터만 추출하여 전송
    func goodMetricsHandling(_ payloads: [MXMetricPayload]) {
        for payload in payloads {
            // 핵심 지표만 추출
            let essentialMetrics = extractEssentialMetrics(payload)
    
            // 사용자 동의 확인
            if UserDefaults.standard.bool(forKey: "analytics_consent") {
                sendToServer(essentialMetrics)
            }
        }
    }
    
    private func extractEssentialMetrics(_ payload: MXMetricPayload) -> [String: Any] {
        var metrics: [String: Any] = [:]
    
        // CPU 사용량
        if let cpuMetrics = payload.cpuMetrics {
            metrics["cpu_time_seconds"] = cpuMetrics.cumulativeCPUTime.converted(to: .seconds).value
        }
        
        // GPU 사용량 (iOS 14+)
        if #available(iOS 14.0, *) {
            if let gpuMetrics = payload.gpuMetrics {
                metrics["gpu_time_seconds"] = gpuMetrics.cumulativeGPUTime.converted(to: .seconds).value
            }
        }
        
        // 앱 실행 시간 (배터리 영향 추정에 중요)
        if let runtimeMetrics = payload.applicationTimeMetrics {
            metrics["foreground_time_seconds"] = runtimeMetrics.cumulativeForegroundTime.converted(to: .seconds).value
            metrics["background_time_seconds"] = runtimeMetrics.cumulativeBackgroundTime.converted(to: .seconds).value
        }
        
        // 메모리 사용량
        if let memoryMetrics = payload.memoryMetrics {
            metrics["peak_memory_mb"] = memoryMetrics.peakMemoryUsage.converted(to: .megabytes).value
            metrics["average_suspended_memory_mb"] = memoryMetrics.averageSuspendedMemory.averageMeasurement.converted(to: .megabytes).value
        }
        
        // 디스크 I/O
        if let diskMetrics = payload.diskIOMetrics {
            metrics["disk_writes_mb"] = diskMetrics.cumulativeLogicalWrites.converted(to: .megabytes).value
        }
        
        // 위치 서비스 사용 (배터리 소모 큼)
        if let locationMetrics = payload.locationActivityMetrics {
            metrics["best_accuracy_location_seconds"] = locationMetrics.cumulativeBestAccuracyForNavigationTime.converted(to: .seconds).value
        }
        
        // 앱 버전 정보
        metrics["app_version"] = Bundle.main.infoDictionary?["CFBundleShortVersionString"]
        metrics["build_number"] = Bundle.main.infoDictionary?["CFBundleVersion"]
        
        // 메타데이터
        if let metadata = payload.metaData {
            metrics["device_type"] = metadata.deviceType
            metrics["os_version"] = metadata.osVersion
            metrics["app_build_version"] = metadata.applicationBuildVersion
            metrics["region"] = metadata.regionFormat
        }
        
        // 타임스탬프
        metrics["timestamp"] = Date().timeIntervalSince1970
        metrics["time_begin"] = payload.timeStampBegin.timeIntervalSince1970
        metrics["time_end"] = payload.timeStampEnd.timeIntervalSince1970
    
        return metrics
    }

     


    Conclusion

    iOS 앱의 배터리 드레인 문제 해결은 측정 → 분석 → 최적화 → 모니터링의 지속적인 사이클이 필요한 작업이에요.

     

    핵심 포인트를 정리해볼까요?

     

    🎯 측정이 모든 것의 시작

    • 실제 기기 배터리 설정에서 확인하는 것이 가장 신뢰할 만함
    • MetricKit으로 실사용자 데이터 수집
    • 수동 배터리 모니터링으로 특정 기능별 테스트

    🎯 백그라운드 작업이 주요 범인

    • Background App Refresh 최적화가 가장 효과적
    • 네트워크 요청 배칭으로 라디오 활성화 최소화
    • 위치 서비스 정확도 조절로 GPS 사용 줄이기

    🎯 시스템과의 협력

    • Low Power Mode 감지하여 동작 조절
    • 배터리 레벨별 적응형 동작 구현
    • iOS의 배터리 보호 정책 이해하고 협력

    🎯 지속적인 모니터링

    • CI/CD에 배터리 체크 통합
    • MetricKit으로 실시간 모니터링 구축
    • 사용자 피드백과 데이터 연결

    가장 중요한 것은 사용자 경험과 배터리 최적화의 균형을 찾는 것입니다.
    무작정 모든 기능을 제한하는 것이 아니라, 정말 필요한 기능은 효율적으로 구현하고, 불필요한 백그라운드 작업은 과감히 제거하는 지혜가 필요해요.
     
    특히 MetricKit은 정말 강력한 도구입니다.
    실사용자들의 실제 배터리 사용 패턴을 볼 수 있어서 개발 환경에서 놓쳤던 문제들을 발견할 수 있거든요.
     
    Xcode 15 환경에서는 예전처럼 쉬운 에너지 측정 도구들이 사라져서 아쉽지만, 대신 실제 기기 테스트 + MetricKit + 수동 모니터링 조합으로 더욱 정확한 배터리 최적화가 가능해졌어요.
     

    여러분의 앱도 배터리 친화적인 앱이 되어 사용자들에게 더 오래 사랑받는 앱이 되길 바랍니다! 🔋✨

     


    References

    How to Debug iOS Crashes with MetricKit - adjoe

    We've been successfully debugging iOS crashes with MetricKit and now we're sharing the whole method - learn straight from adjoe's engineers.

    adjoe.io

    MetricKit

    At WWDC this year, Apple announced a coordinated effort between Xcode 11 and iOS 13 to bring new insights to developers about how their apps are performing in the field.

    nshipster.com

    Common Reasons for Background Tasks to Fail in iOS

    Learn what are the common reasons your background tasks never get executed, and how to go around them.

    www.andyibanez.com

    Improving Battery Life and Performance - WWDC19 - Videos - Apple Developer

    Learn about new ways to find and fix performance issues during daily development, beta testing, and public release on the App Store...

    developer.apple.com

    Background Tasks | Apple Developer Documentation

    Support background processing in your app by wrapping your app’s most critical work in framework-provided tasks.

    developer.apple.com

    MetricKit | Apple Developer Documentation

    Aggregate and analyze per-device reports on exception and crash diagnostics and on power and performance metrics.

    developer.apple.com

    Energy Efficiency Guide for iOS Apps: Energy Efficiency and the User Experience

    Energy Efficiency Guide for iOS Apps

    developer.apple.com

    Analyzing your app’s battery use | Apple Developer Documentation

    Increase the available use time for your app on a single battery charge by reducing your appʼs power consumption.

    developer.apple.com

    'iOS' 카테고리의 다른 글

    WKURLSchemeHandler  (0) 2025.11.09
    Mach-O 파일 구조 분석해보며 최적화 해보기  (2) 2025.09.13
    Diet for iOS App size (feat. App Thinning)  (5) 2025.08.02
    Meet PaperKit (feat. WWDC 2025)  (0) 2025.06.25
    Logger Caching & Performance  (6) 2025.06.04
Designed by Tistory.