← 모든 글

HealthKit 운동 라이프사이클: HKWorkoutSession 상태와 iOS 26 크로스 플랫폼 표면

HKWorkoutSession은 HealthKit의 운동 상태 머신입니다. 세션은 6개 상태(.notStarted, .prepared, .running, .paused, .stopped, .ended)를 거치며, HKWorkoutSessionDelegate를 통해 라이프사이클 이벤트를 노출하고, (iOS 26부터) Apple Watch뿐 아니라 iPhone에서도 실행됩니다1. 세션과 짝을 이루는 HKLiveWorkoutBuilder는 샘플과 이벤트를 점진적으로 수집합니다. iOS 26은 동일한 빌더 API를 iPhone으로 가져왔습니다. 이 클러스터의 watchOS Runtime Contract 글은 watchOS 앱이 백그라운드에서 계속 실행되려면 인식된 세션 유형이 필요하다고 주장했습니다. HKWorkoutSession은 그러한 세션 유형 중 하나이며, 라이프사이클 상태는 런타임 모델에 직접 매핑됩니다.

이 글은 Apple 문서를 따라 운동 라이프사이클을 살펴봅니다. 프레임은 “각 상태가 무엇을 허용하고 각 전환이 무엇을 트리거하는가”입니다. 라이프사이클을 잘못 관리하는 운동 앱은 데이터를 잃거나(running 상태를 너무 일찍 벗어남) 배터리를 소모하기(running 상태에서 전혀 벗어나지 않음) 때문입니다.

TL;DR

  • HKWorkoutSession 라이프사이클: notStartedpreparedrunning → (선택적으로 pausedrunning) → stoppedended. 전환은 HKWorkoutSessionDelegate.workoutSession(_:didChangeTo:from:date:)을 통해 보고됩니다2.
  • HKLiveWorkoutBuilder는 운동 세션과 짝을 이루는 라이브 데이터 누산기입니다. 2018년 watchOS에서 시작되었고 iOS 26+, iPadOS 26+, Mac Catalyst 26+에 출시되었습니다. iPhone 운동은 Apple Watch와 동일한 HKLiveWorkoutBuilder API를 사용하며, 런타임 모델과 센서 가용성에서 플랫폼별 차이가 있습니다3.
  • 세션의 prepare() 메서드는 startActivity(_:)가 실제 운동을 실행하기 전에 센서를 워밍업합니다. Apple의 권장사항은 prepare()startActivity(_:) 사이에 3초 카운트다운 UI를 두어 심박수 센서와 외부 Bluetooth 디바이스가 연결될 시간을 주는 것입니다.
  • stopped 상태는 일시적입니다. 앱은 이 상태에서 메트릭을 마무리할 수 있지만 세션을 재개할 수는 없습니다. end()를 호출하면 종단 상태인 ended로 전환됩니다.
  • 이 클러스터의 watchOS Runtime Contract 글은 운동 세션이 손목 떨어짐 상황에서도 watchOS 앱을 계속 실행시키는 방법을 다룹니다. 라이프사이클 상태 머신과 런타임 유지 계약은 동일한 표면의 양면입니다.

6개의 상태

HKWorkoutSessionState는 라이프사이클을 열거합니다2:

.notStarted. 세션이 생성되었지만 준비되지 않은 상태입니다. 센서는 워밍업되지 않았고, 앱은 아직 활성 운동 호스트로 간주되지 않습니다. .prepared로의 전환은 앱이 prepare()를 호출할 때 일어납니다.

.prepared. 세션이 prepare()를 호출한 상태입니다. 센서가 워밍업 중이지만 운동은 아직 시작되지 않았습니다. 심박수 모니터가 연결되고, 모션 센서가 초기화되며, GPS가 신호를 잡습니다. 사용자 측 패턴은 3초 카운트다운(“준비… 3, 2, 1, 시작!”)입니다. 이 시간 동안 시스템이 깨끗한 신호를 확보할 시간을 갖기 때문에 running 상태의 첫 메트릭이 정확해집니다.

.running. 활성 운동 상태입니다. 앱은 메트릭을 수집하고, 라이브 데이터를 표시하며, (watchOS에서는) workout-active 런타임 계약을 통해 화면을 켜진 상태로 유지합니다. .running으로의 전환은 startActivity(_:)를 통해 일어납니다.

.paused. 사용자가 일시정지한 상태입니다. 앱은 더 이상 활성 메트릭(예: 거리)을 수집하지 않지만 세션은 보존됩니다. resume()을 호출하면 .running으로 돌아갑니다. 일시정지/재개 사이클은 단일 세션 내에서 몇 번이든 일어날 수 있습니다.

.stopped. 일시적인 운동 후 상태입니다. 세션은 활성 단계를 종료했지만 아직 마무리되지 않았습니다. 라이브 빌더는 여전히 메트릭을 마무리할 수 있습니다. .stopped에서 end()를 호출하면 .ended로 전환됩니다. 앱은 .stopped에서 재개할 수 없습니다.

.ended. 종단 상태입니다. 세션이 완료되었고, 라이브 빌더는 마무리하라는 명령을 받았으며, 운동은 (앱이 빌더에서 finishWorkout(completion:)을 호출했다면) HealthKit에 저장됩니다. .ended 상태가 되면 세션은 더 이상 조작할 수 없습니다.

상태 다이어그램에는 한 가지 구체적인 함정이 있습니다. .stopped에서 .running으로 돌아가는 경로가 없다는 것입니다. 사용자가 “종료를 취소”하고 싶은 운동은 새 세션을 시작해야 하며, 이전 세션을 재개할 수는 없습니다.

전환을 일으키는 메서드

HKWorkoutSession은 상태 전환을 위해 다음 메서드들을 노출합니다1:

  • prepare(). .notStarted에서 .prepared로 전환합니다. 센서를 워밍업합니다.
  • startActivity(with: Date). .prepared에서 .running으로 전환합니다. Date 파라미터로 앱이 공식 시작 시간(보통 .now)을 설정할 수 있습니다.
  • pause(). .running에서 .paused로 전환합니다.
  • resume(). .paused에서 .running으로 다시 전환합니다.
  • stopActivity(with: Date). .running(또는 .paused)에서 .stopped로 전환합니다. Date는 공식 종료 시간입니다.
  • end(). .stopped에서 .ended로 전환합니다.

prepare()에서 startActivity(_:)까지의 패턴은 워밍업 윈도우입니다. stopActivity(_:)에서 end()까지의 패턴은 정리 윈도우입니다. 라이브 빌더가 세션이 종료되기 전에 마지막 샘플을 추가할 기회를 얻습니다.

watchOS와 iPhone에서의 HKLiveWorkoutBuilder

HKLiveWorkoutBuilder는 세션과 짝을 이루는 라이브 데이터 누산기입니다3. 이 빌더는 watchOS 5에서 watchOS에 출시되었고 iOS 26+, iPadOS 26+, Mac Catalyst 26+로 확장되었습니다. 빌더의 라이프사이클은 세션의 라이프사이클과 짝을 이룹니다:

let configuration = HKWorkoutConfiguration()
configuration.activityType = .running
configuration.locationType = .outdoor

let session = try HKWorkoutSession(healthStore: store, configuration: configuration)
let builder = session.associatedWorkoutBuilder()
builder.dataSource = HKLiveWorkoutDataSource(healthStore: store, workoutConfiguration: configuration)

session.delegate = self
builder.delegate = self

session.prepare()
// User taps Start after countdown
session.startActivity(with: Date())
try await builder.beginCollection(at: Date())

// During the workout, metrics flow into the builder via the data source.
// builder.collectedTypes contains the sample types being collected.
// builder.statistics(for:) returns running stats.

// User ends the workout
session.stopActivity(with: Date())
try await builder.endCollection(at: Date())
let workout = try await builder.finishWorkout()
session.end()

세 가지 요소가 운동을 하나로 묶습니다:

  • HKWorkoutConfiguration은 활동 유형과 위치를 지정합니다. 활동 유형은 메트릭 선택을 결정합니다(러닝 운동은 페이스를 수집하지만, 실내 사이클링 운동은 그렇지 않습니다).
  • HKLiveWorkoutDataSource는 세션의 센서 구성과 빌더의 데이터 누적 사이의 다리입니다. 데이터 소스는 샘플을 게시하고, 빌더는 이를 수신하여 저장합니다.
  • HKLiveWorkoutBuilder는 진행 중인 운동의 상태를 보유하고 저장된 HKWorkout 객체를 마무리합니다.

패턴은 점진적입니다. 샘플은 .running 상태 동안 지속적으로 흐르고, 빌더는 이를 누적 통계에 통합하며, 최종 finishWorkout()이 완전한 운동을 HealthKit에 기록합니다.

iOS 26: iPhone에서의 운동

iOS 26은 watchOS가 사용하는 것과 동일한 HKLiveWorkoutBuilder 및 데이터 소스 API와 함께 HKWorkoutSession을 iPhone에 도입했습니다4. 구성은 동일합니다. 플랫폼별 차이는 API 표면이 아니라 런타임 모델, 센서 가용성, 프라이버시 처리에 있습니다.

iOS 26의 운동 API가 가능하게 하는 사용 사례: - Phone-as-companion 운동 앱(iPhone이 Watch 세션과 함께 심박수 모니터 데이터를 보유합니다). - iPhone 전용 피트니스 앱: Apple Watch가 없는 사용자를 위한 것으로, iPhone이 내장 센서와 연결된 액세서리를 통해 세션을 추적합니다. - 크로스 디바이스 세션 연속성: Apple Watch 세션이 iPhone으로 핸드오프되거나(사용자가 Watch를 벗지만 iPhone이 계속 추적하기를 원할 때) 그 반대의 경우.

언급할 만한 플랫폼 차이: - 센서 가용성. iPhone에는 가속도계와 GPS가 있지만 내장 심박수 센서는 없습니다. iOS 운동에서 심박수가 필요한 앱은 Bluetooth 심박수 스트랩과 페어링하거나 HealthKit을 통해 연결된 Apple Watch에서 읽어옵니다. - 런타임 모델. Apple Watch의 workout-active 런타임은 손목 떨어짐 동안에도 지속적인 센서 접근을 보장합니다. iPhone의 런타임은 시스템의 일반 포그라운드/백그라운드 라이프사이클과 씬 델리게이트를 통한 크래시 복구(WWDC 2025 세션 322에서 다룸)에 의존하며, 이는 다른 형태의 보장입니다. - 프라이버시와 잠금 화면 동작. 디바이스가 잠긴 상태에서 실행되는 iPhone 운동은 샘플 수집을 계속하기 위해 명시적인 구성이 필요합니다. 잠금 화면이 손목 떨어짐보다 더 강한 프라이버시 경계이기 때문입니다.

델리게이트 프로토콜

HKWorkoutSessionDelegate는 상태 전환과 오류를 보고합니다5:

extension WorkoutCoordinator: HKWorkoutSessionDelegate {
    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didChangeTo toState: HKWorkoutSessionState,
        from fromState: HKWorkoutSessionState,
        date: Date
    ) {
        switch toState {
        case .running:
            // workout is active
        case .paused:
            // user paused
        case .stopped:
            // finalize metrics
        case .ended:
            // workout done; cleanup
        default:
            break
        }
    }

    func workoutSession(
        _ workoutSession: HKWorkoutSession,
        didFailWithError error: Error
    ) {
        // session failed (e.g., heart rate sensor disconnected unexpectedly)
    }
}

델리게이트는 상태 전환에 대한 단일 진실 공급원입니다. 메서드 호출에서 상태를 추론하는 앱(startActivity()를 호출했으니 이제 running 상태다`)은 시스템이 적용하는 상태 변경(사용자가 정지해 있을 때 자동 일시정지, Watch가 제거될 때 자동 종료)을 놓칩니다. 델리게이트 기반 패턴이 옳습니다.

런타임 계약

HKWorkoutSession은 마음챙김, 알람, 오디오 녹음 세션과 함께 watchOS가 앱을 백그라운드에서 계속 실행시키기 위해 인식하는 세션 유형 중 하나입니다6. 계약: 세션이 .prepared, .running, .paused, .stopped 상태에 있는 동안 앱은 계속 실행되고, 사용자가 손목을 들면 화면이 깨어나며, 센서는 앱으로 지속적으로 스트리밍됩니다.

이 클러스터의 watchOS Runtime Contract 글이 이를 자세히 다룹니다. 운동 앱에 관련된 요점: 라이프사이클 상태 머신은 watchOS에 “이 앱을 계속 실행시켜라”라고 알려주는 것이며, .ended로의 전환은 계약을 해제하고 OS가 앱을 일시중단하도록 허용합니다.

실용적인 함의: 운동 세션을 조기에 종료하지 마세요. 사용자가 전화 통화 때문에 운동에서 잠시 떠났다가 돌아온다면, 세션은 .running 상태로 남아있어야 하며(또는 pause()를 통해 일시정지되어야 하며), 종료되어서는 안 됩니다. 종료 후 재시작은 데이터와 런타임 연속성을 잃습니다.

흔한 실패

운동 앱 실패 로그에서 나오는 세 가지 패턴:

prepare() 건너뛰기. prepare()를 먼저 호출하지 않고 startActivity(_:)를 호출하는 앱은 처음 5-10초의 심박수 데이터가 신뢰할 수 없거나(센서가 워밍업되지 않음) 누락된(Bluetooth 심박수 스트랩이 연결되지 않음) 운동을 만듭니다. 해결책: 항상 prepare()를 호출하고, 짧은 카운트다운 UI를 표시한 다음 startActivity(_:)를 호출하세요.

.running에서 end()를 직접 호출. .stopped를 건너뛰면 메트릭 마무리 윈도우를 건너뜁니다. 라이브 빌더가 세션 종료 전에 마지막 샘플을 처리하지 못해 요약 통계가 누락될 수 있습니다. 해결책: 항상 stopActivity(_:)를 먼저 호출하고, 델리게이트 콜백이 .stopped를 확인할 때까지 기다린 다음 end()를 호출하세요.

델리게이트 대신 상태 추론. 로컬 상태(isWorkoutActive: Bool)를 추적하면서 델리게이트를 연결하지 않는 앱은 시스템 주도 전환(자동 일시정지, Watch 제거 시 자동 종료, 오류 상태)을 놓칩니다. 해결책: 항상 델리게이트를 진실의 원천으로 사용하세요.

이 패턴이 iOS 26+ 앱에 의미하는 것

세 가지 시사점.

  1. 라이프사이클을 UI 상태에 명시적으로 매핑하세요. 운동 앱의 UI는 명백한 상태들을 가집니다: 시작 전, 준비 중, 활성, 일시정지, 요약, 완료. 각각을 HKWorkoutSessionState에 매핑하세요. UI를 임시 boolean으로 실행하지 말고, 델리게이트를 통해 세션이 보고하는 상태에 바인딩하세요.

  2. 메트릭을 표시하는 모든 세션에 prepare() + 카운트다운 UI를 사용하세요. 3초 워밍업은 사용자가 신뢰하는 데이터와 사용자가 무시하는 데이터의 차이입니다. 비용은 작은 UI 요소이고, 이득은 신뢰할 수 있는 메트릭입니다.

  3. iOS 26의 iPhone 운동 세션에는 다른 빌더 코드가 필요합니다. 세션 API는 공유되지만, 빌더 측은 플랫폼별입니다. iOS와 watchOS 간에 코드 경로를 공유하는 앱은 명시적인 #if os(watchOS) 분기 또는 차이를 추상화하는 래퍼가 필요합니다.

전체 Apple Ecosystem 클러스터: 타입드 App Intents; MCP 서버; 라우팅 질문; Foundation Models; 런타임 vs 툴링 LLM 구분; 세 가지 표면; 단일 진실 공급원 패턴; 두 개의 MCP 서버; Apple 개발용 훅; Live Activities; watchOS 런타임; SwiftUI 내부; RealityKit의 공간 정신 모델; SwiftData 스키마 규율; Liquid Glass 패턴; 멀티플랫폼 출시; 플랫폼 매트릭스; Vision 프레임워크; Symbol Effects; Core ML 추론; Writing Tools API; Swift Testing; Privacy Manifest; 플랫폼으로서의 접근성; SF Pro 타이포그래피; visionOS 공간 패턴; Speech 프레임워크; SwiftData 마이그레이션; tvOS 포커스 엔진; @Observable 내부; SwiftUI Layout 프로토콜; 커스텀 SF Symbols; AVFoundation HDR; 내가 쓰지 않는 것들. 허브는 Apple Ecosystem Series에 있습니다. AI 에이전트와 함께하는 더 광범위한 iOS 컨텍스트는 iOS Agent Development guide를 참조하세요.

FAQ

iPhone은 Apple Watch와 동일한 HKLiveWorkoutBuilder를 사용하나요?

iOS 26부터 그렇습니다. 동일한 HKLiveWorkoutBuilder API가 iOS 26+, iPadOS 26+, Mac Catalyst 26+, watchOS 5+에 출시됩니다. 플랫폼 차이는 런타임 모델과 센서 가용성에 있으며, 빌더 API에는 없습니다. iPhone 운동은 씬 델리게이트(WWDC 2025 세션 322 참고)를 통해 잠금 화면 프라이버시와 크래시 복구를 처리하며, 이는 watchOS의 손목 떨어짐 런타임 보장과 다르지만, 데이터 누적 API는 동일합니다.

최대 운동 시간은 얼마인가요?

엄격한 시간 제한은 없습니다. 실용적 한계는 배터리(Apple Watch는 지속적 운동에서 약 6-8시간 지속)와 저장공간(고주파 데이터를 가진 운동은 빠르게 누적됨)에서 옵니다. 마라톤 러닝 앱(12시간 이상 운동)은 오늘날에도 출시되고 있으며, 프레임워크가 이를 지원합니다.

자동 일시정지를 어떻게 처리하나요?

HKWorkoutConfiguration.activityType을 자동 일시정지를 지원하는 유형(예: .running)으로 설정하세요. watchOS는 사용자의 움직임에 따라 자동으로 일시정지하고 재개합니다. 상태 전환은 델리게이트를 통해 흐르며, 사용자가 시작한 일시정지와 동일하게 처리하세요.

사용자가 운동 중간에 Watch를 벗으면 어떻게 되나요?

세션은 현재 상태(보통 .running)로 계속됩니다. watchOS 시스템은 Watch가 손목에서 너무 오래 떨어져 있으면 결국 세션을 종료합니다. 이때 델리게이트의 didFailWithError 콜백이 발생합니다. 크로스 디바이스 세션을 가진 앱(iOS 26+)은 사용자가 두 디바이스를 모두 가지고 있다면 iPhone으로 핸드오프할 수 있습니다.

운동을 HealthKit에 저장해야 하나요?

거의 항상 그렇습니다. builder.finishWorkout(completion:)을 호출하면 완전한 운동이 HealthKit에 기록되며, 이는 데이터가 활동 앱, 건강 앱의 운동 목록, 사용자가 권한을 부여한 다른 앱에 나타난다는 의미입니다. 저장을 건너뛰면 데이터가 폐기되고, 프레임워크는 복구 경로를 제공하지 않습니다.

이것은 다른 최근 Apple Health 추가사항과 어떤 관련이 있나요?

iOS 26 / watchOS 26은 운동 API를 두 가지 구체적인 방식으로 확장했습니다. 첫째, HKWorkoutSession을 iPhone에 도입(위에서 다룸). 둘째, 활동 유형 목록과 자동 감지 범위를 확장. 이 클러스터의 watchOS Runtime Contract 글은 런타임 측면을 다루고, 이 글은 라이프사이클 측면을 다룹니다. 함께 보면 운동 앱을 출시하는 전체 표면을 설명합니다.

References


  1. Apple Developer Documentation: HKWorkoutSession. 상태 전환, 구성, 델리게이트 프로토콜이 있는 세션 클래스. 

  2. Apple Developer Documentation: HKWorkoutSessionState. 다섯 가지 상태 케이스(.notStarted, .prepared, .running, .paused, .stopped, .ended)와 그 의미. 

  3. Apple Developer Documentation: HKLiveWorkoutBuilderHKLiveWorkoutDataSource. 라이브 빌더 API(watchOS 5+, iOS 26+, iPadOS 26+, Mac Catalyst 26+)와 그 데이터 소스. 

  4. Apple Developer: Track workouts with HealthKit on iOS and iPadOS (WWDC 2025 세션 322). HKWorkoutSession의 iPhone 확장 (iOS 26). 

  5. Apple Developer Documentation: HKWorkoutSessionDelegate. 상태 전환 및 오류 콜백이 있는 델리게이트 프로토콜. 

  6. Apple Developer Documentation: Background Execution on watchOS. 손목 떨어짐 동안 어떤 세션 유형이 앱을 계속 실행시키는지 설명하는 watchOS 런타임 계약. 

관련 게시물

HealthKit + SwiftUI on iOS 26: Authorization, Sample Types, and Cross-Platform Patterns

Real production patterns from Water (water tracking, HKQuantitySample) and Return (mindful sessions, HKCategorySample). …

17 분 소요

watchOS Runtime Is a Contract, Not a Background Task

watchOS does not have iOS's background. WKExtendedRuntimeSession is a contract you sign with the system, broken on wrist…

15 분 소요

The Cleanup Layer Is the Real AI Agent Market

Charlie Labs pivoted from building agents to cleaning up after them. The AI agent market is moving from generation to pr…

15 분 소요