← 모든 글

watchOS 런타임은 백그라운드 작업이 아닌 계약입니다

Return의 Watch 앱은 사용자가 손목을 내려도 카운팅을 계속해야 하는 멀티 사이클 명상 타이머를 실행합니다.1 그 제약 조건에서 살아남은 패턴이 바로 WKExtendedRuntimeSession과 전역 앱 범위 델리게이트입니다. 그 외 모든 것은 워치가 슬립 상태에 들어가는 순간 죽습니다.

watchOS는 화면이 작아진 iOS가 아닙니다. 런타임 모델 자체가 다릅니다. iOS는 앱에 넉넉한 포그라운드 예산을 제공하고, 오디오 세션, 위치 업데이트, BGTaskScheduler 및 그 외 몇 가지 어포던스를 통해 점차 줄어들지만 실재하는 백그라운드 런타임을 제공합니다.2 watchOS는 손목을 내린 후 포그라운드 앱에 초 단위로 측정되는 예산을 주고, 그 후에는 시스템과 런타임 계약을 체결하지 않은 한 앱이 일시 중지됩니다. “그냥 백그라운드에서 뭔가 하고 있다”는 식의 어포던스는 존재하지 않습니다. “운동, 마음챙김 세션, 스마트 알람, 내비게이션 경로, 또는 건강 모니터링 작업을 실행 중이다”가 있을 뿐, 그 외에는 아무것도 없습니다.3

Return의 Watch 타겟은 마음챙김 타이머입니다. 세션 계약은 WKBackgroundModes: mindfulness입니다. 런타임 API는 WKExtendedRuntimeSession입니다. Watch 앱을 손목 내림에서 깨짐 상태에서 25분 명상에서도 살아남는 상태로 바꾼 패턴이 바로 이 글에서 설명하는 것입니다.

TL;DR

  • watchOS에는 iOS 스타일의 백그라운드가 없습니다. 포그라운드 런타임은 손목을 내린 직후 곧 종료되며, 등록된 세션 타입만이 계속 실행됩니다.
  • WKExtendedRuntimeSession이 API 인터페이스입니다. Apple은 네 가지 세션 타입을 지원합니다: self-care, mindfulness, physical-therapy, alarm. 명상 타이머의 경우 세션 타입은 mindfulness이며, Info.plistWKBackgroundModes를 통해 선언합니다.
  • 세션 매니저는 뷰 범위가 아닌 앱 범위에 살아 있어야 합니다. SwiftUI의 뷰 라이프사이클은 내비게이션 시 뷰 소유 객체를 해제합니다. 해제된 세션 델리게이트는 세션 자체가 여전히 실행 중이더라도 죽은 세션이나 다름없습니다.
  • WKExtendedRuntimeSessionDelegate 콜백이 바로 그 계약입니다: didStart, willExpire, didInvalidateWith. 만료 콜백은 시스템이 강제 무효화를 수행하기 전에 호출됩니다. Apple의 “Using extended runtime sessions” 샘플 코드는 이를 “세션이 끝나기 전에 작업을 마무리하고 정리하는” 위치로 설명합니다.3
  • 활성 확장 세션 없이 손목을 내리면 타이머가 일시 중지됩니다. 활성 확장 세션이 있는 상태에서 손목을 내리면 타이머가 계속됩니다. 이 세션이 “출시된 제품”과 “두 번째 사용에서 깨지는 제품”의 차이입니다.

watchOS가 iOS 방식으로 해결해 주지 않는 백그라운드 문제

iOS 앱은 화면이 꺼진 상태에서도 앱을 계속 실행해야 할 때 여러 백그라운드 어포던스에 의존합니다.2

  • .playback 카테고리의 AVAudioSession은 음악이 재생되는 동안 오디오 앱을 계속 살아 있게 합니다.
  • CLLocationManager 백그라운드 업데이트는 파란 막대와 함께 내비게이션 앱을 살아 있게 유지합니다.
  • BGTaskScheduler는 시스템이 자체 일정에 따라 스케줄링하는 짧은 유지 작업을 큐에 넣습니다.
  • 포그라운드 UI 익스텐션(Live Activity, CallKit, PushKit)은 앱 프로세스를 시스템이 제어하는 렌더링 표면에 연결합니다.

watchOS에서는 그 어느 것도 여러분이 가정할 수 있는 방식으로 도움이 되지 않습니다. Watch 앱에는 동일한 백그라운드 작업 스케줄러가 없습니다. 무음 상태에서도 타이머가 카운팅을 유지하게 해 주는 백그라운드된 AVAudioSession.playback 모드도 없습니다. “사용자가 손목을 내린 후에도 계속 실행하고 싶다”에 해당하는 단 하나의 구조적 프리미티브가 있는데, 그 프리미티브가 바로 선언된 세션 타입을 가진 WKExtendedRuntimeSession입니다.3

Apple이 WKExtendedRuntimeSession에 대해 지원하는 세션 타입은 의도적으로 좁습니다.3

  • self-care (간단한 웰빙 활동, 포그라운드 런타임, 10분 제한)
  • mindfulness (조용한 명상, 포그라운드 런타임, 1시간 제한)
  • physical-therapy (스트레칭과 가동 범위 운동, 백그라운드 런타임, 1시간 제한)
  • alarm (스마트 알람, 백그라운드 런타임, 30분 제한, start(at:)을 통해 최대 36시간 앞서 스케줄 가능)

운동 앱은 별도의 workout-processing 백그라운드 모드와 함께 HKWorkoutSession을 사용합니다. 이 경로는 실제 운동에 대해 문서화되어 있으며 WKExtendedRuntimeSession 타입이 아닙니다.4 underwater-depth 백그라운드 모드는 다이브 세션 API 경로를 통해 다이빙 및 수심 추적 앱을 지원하며, 이것 역시 WKExtendedRuntimeSession을 통하지 않습니다. 앱은 workout-processing을 하나의 확장 런타임 세션 타입과 결합할 수 있지만, 앱당 둘 이상의 확장 런타임 타입을 선택할 수는 없습니다.4

이러한 카테고리 중 어느 것에도 해당하지 않는 앱은 손목을 내린 후 실행을 위해 WKExtendedRuntimeSession을 사용할 수 없습니다. 오디오 앱은 다른 코드 경로에서 AVAudioSession.Category.playback 오디오 세션 카테고리와 Now Playing 통합에 의존하고, 내비게이션 앱은 CLLocationManager 백그라운드 업데이트를 사용합니다. Watch는 범용 컴퓨터가 아니라, 런타임 모델이 강제하는 배터리 제약을 가진 기기입니다.

명상 타이머는 mindfulness에 들어맞습니다. 계약 내용은 다음과 같습니다: Info.plist에 백그라운드 모드를 선언하고, WKExtendedRuntimeSession을 요청하고, 델리게이트 콜백을 처리하고, 타이머가 종료되면 세션을 종료합니다. Apple은 마음챙김 세션 한도를 1시간으로 문서화하며, 시스템 재량으로 열적 또는 배터리 압력 상황에서 단축될 수 있다고 명시합니다.3

Return에 적용된 패턴

이 패턴은 Info.plist 선언으로 시작합니다.4

<key>WKBackgroundModes</key>
<array>
    <string>mindfulness</string>
</array>

이 모드 선언이 세션 타입을 유효하게 만듭니다. 이것 없이 WKExtendedRuntimeSession().start()를 호출하면 조용히 실패하고, 백그라운드 모드가 전혀 없는 Watch 앱과 마찬가지로 손목을 내리는 순간 앱이 일시 중지됩니다.

세션 매니저 자체는 앱 범위에 살아 있어야 합니다. SwiftUI의 뷰 라이프사이클은 오래 살아 있어야 하는 상태 객체에 비우호적입니다: @StateObject@State는 그것을 소유한 뷰에 한정되며, 뷰를 교체하는 내비게이션 푸시는 그 상태도 함께 떨어뜨립니다. 델리게이트가 세션 중간에 해제되는 WKExtendedRuntimeSession은 크래시를 발생시키지 않습니다. 세션은 계속 실행되지만, 델리게이트 콜백(willExpire, didInvalidateWith)은 해제된 객체에 도달하므로 정리가 절대 일어나지 않으며, 그래서 다음 startSession() 호출은 활성 세션이 없다고 판단하고 중복 세션을 시작합니다.

출시된 패턴은 앱 범위의 싱글톤입니다. 아래 스니펫은 구조적 형태이며, 프로덕션에서는 관찰성을 위해 각 메서드 내부에 로깅을 추가합니다.

import SwiftUI
import WatchKit

final class WatchSessionManager: NSObject, WKExtendedRuntimeSessionDelegate {
    static let shared = WatchSessionManager()

    private var session: WKExtendedRuntimeSession?

    private override init() {
        super.init()
    }

    var isSessionActive: Bool {
        session != nil
    }

    func startSession() {
        guard session == nil else { return }
        let newSession = WKExtendedRuntimeSession()
        newSession.delegate = self
        newSession.start()
        session = newSession
    }

    func endSession() {
        guard let existing = session else { return }
        existing.invalidate()
        session = nil
    }

    // MARK: - WKExtendedRuntimeSessionDelegate

    func extendedRuntimeSessionDidStart(_ session: WKExtendedRuntimeSession) {}

    func extendedRuntimeSessionWillExpire(_ session: WKExtendedRuntimeSession) {
        // Apple's "about to expire / finish and clean up" hook
    }

    func extendedRuntimeSession(
        _ session: WKExtendedRuntimeSession,
        didInvalidateWith reason: WKExtendedRuntimeSessionInvalidationReason,
        error: Error?
    ) {
        self.session = nil
    }
}

프로토콜 준수를 넘어 세 가지 구조적 세부 사항이 중요합니다.

static let shared 인스턴스는 정적 저장소를 통해 워치 앱의 프로세스 수명 동안 유지됩니다. ARC가 이를 해제하지 않습니다. App 레벨 바인딩이 가져다주는 것은 추가 보유가 아니라 안정적인 관찰 지점입니다. 이 패턴이 방지하는 버그 패턴은 다음과 같습니다: 세션 중간에 팝되는 일시적 뷰만이 보유한 세션 매니저에서, 뷰는 죽지만 static let shared는 살아남고, 그 부수 효과로 @StateObject로 래핑된 매니저는 관찰 사이클을 잃고 올바르게 다시 렌더링하지 못합니다. UI가 정식 인스턴스를 계속 관찰하도록 싱글톤과 App 레벨의 @Observable 접근자를 함께 사용하세요.

session 프로퍼티는 중복 세션에 대한 보호 장치입니다. “다시 시작” 버튼이 있는 타이머는 여러 경로에서 startSession()을 호출할 수 있습니다. guard session == nil 체크가 잠금장치 역할을 합니다. 두 개의 동시 확장 세션은 예측 불가능한 동작을 일으킵니다: 때로는 두 번째가 성공하고 첫 번째가 고아가 되며, 때로는 시작 호출이 조용히 실패합니다. 단일 세션 불변식이 이 모든 클래스를 방지합니다.

델리게이트 콜백은 로깅하지만 거의 행동하지 않습니다. didStart 콜백은 세션당 한 번 발생하며 관찰성에 유용한 훅입니다. willExpire 콜백은 시스템이 강제 무효화를 수행하기 전에 발생하며, Apple 샘플은 앱이 “세션이 끝나기 전에 작업을 마무리하고 정리하는” 위치로 기대합니다. didInvalidateWith 콜백은 다음 startSession() 호출이 작동하도록 세션 참조가 정리되는 위치입니다. 출시된 패턴은 콜백이 직접 작업을 수행하는 것이 아니라 콜백은 상태를 업데이트하고, 상태 머신이 작업을 수행하는 것입니다.

타이머 매니저는 타이머가 능동적으로 카운팅 중인지 여부를 변경하는 모든 전환 시점에서 세션 매니저를 호출합니다.

@Observable final class WatchTimerManager {
    func start() {
        startExtendedSession()        // -> WatchSessionManager.shared.startSession()
        // ... start the timer state machine ...
    }

    func pause() {
        timer?.invalidate()
        isRunning = false
        endExtendedSession()          // -> WatchSessionManager.shared.endSession()
    }

    func reset() {
        // ... clear timer state ...
        endExtendedSession()
    }

    private func completeCycle() {
        // ... last cycle handling ...
        endExtendedSession()          // ends on final completion
    }
}

세션은 일시 중지, 리셋, 그리고 마지막 사이클 완료 시 종료됩니다. 제품 추론은 다음과 같습니다: 일시 중지된 명상은 시스템이 mindfulness에 따라 부여하는 런타임 예산을 계속 점유할 필요가 없습니다. 일시 중지에서 재개하면 새로운 세션을 다시 획득합니다. 제품적 비용은 손목을 내린 채 일시 중지된 상태는 손목을 들어 올리는 것만으로는 재개할 수 없다는 점입니다. 사용자가 재개하려면 앱을 포그라운드로 다시 가져와야 합니다. 제품적 이득은 일시 중지된 타이머의 배터리 비용이 0으로 수렴하고, 시스템이 오래된 세션을 보지 않게 된다는 점입니다.

손목 내림이 곧 테스트입니다

시뮬레이터에서의 watchOS 테스팅은 정중한 허구입니다. 시뮬레이터는 실제 Apple Watch가 강제하는 방식으로 손목 내림 런타임 모델을 강제하지 않습니다. 시뮬레이터는 시뮬레이터 창이 포커스를 가지고 있는 한 앱을 포그라운드에 유지합니다. 시뮬레이터의 확장 런타임 세션은 세션이 전혀 없는 것과 동일하게 보이는데, 포그라운드 앱이 어쨌든 계속 실행되기 때문입니다.

실제 테스트는 진짜 Apple Watch에서 해야 합니다.5

  1. 타이머를 실행합니다.
  2. 손목을 내립니다(또는 사이드 버튼을 눌러 화면을 잠급니다).
  3. 30초 동안 기다립니다.
  4. 손목을 다시 들어 올립니다.

활성 확장 런타임 세션 없이는 워치 앱이 일시 중지되며, 타이머 상태는 손목을 내린 순간에 동결되었다가 그 동결 상태에서 재개됩니다. 사용자가 눈을 감고 진행하는 5분 명상의 경우, 눈을 감고 있던 시간만큼 타이머가 어긋날 때까지 버그가 보이지 않습니다.

활성 확장 런타임 세션이 있으면 타이머는 계속 카운팅합니다. 손목을 들면 정확한 경과 위치에 있는 타이머가 드러납니다. 오디오 큐(타이머가 완료 시 재생하는 경우)는 손목을 든 시각이 아니라 정확한 벽시계 시각에 발생합니다.

손목 내림 시나리오는 Return의 첫 번째 Watch 빌드가 가지고 출시되었던 버그였고, 싱글톤 리팩토링이 이를 수정했습니다. 수정 내용은 위의 싱글톤 패턴이며, 버그는 내비게이션 푸시에서 해제되어 버린 SwiftUI 뷰가 보유하던 WatchSessionManager 인스턴스였습니다. 세션은 시스템 측에서는 기술적으로 실행 중이었지만 델리게이트는 해제되었습니다. 매니저의 session 프로퍼티가 이제는 죽은 객체에 설정되어 있었기 때문에 다음 세션 시작 호출은 조용히 노옵(no-op)이 되었습니다. 실제 기기 테스트는 이 실패를 수 초 안에 표면으로 드러냅니다. 시뮬레이터 테스트는 절대로 드러내지 않습니다.

델리게이트 콜백이 실제로 알려주는 것

WKExtendedRuntimeSessionInvalidationReason은 세션이 종료되는 방식을 열거합니다.6

사유 발생 시점
none 앱이 invalidate()를 호출하여 세션을 명시적으로 무효화함
sessionInProgress 동일한 타입의 세션이 이미 실행 중임
expired 시스템이 부과한 시간 한도에 도달함
resignedFrontmost 세션이 실행되는 동안 다른 앱이 포그라운드가 됨
suppressedBySystem 시스템이 세션을 억제함(저전력, 열적 압력)
error 복구할 수 없는 오류가 발생함; error 매개변수를 확인

제품 설계에 중요한 사유는 다음과 같습니다.

expired는 시스템이 부과한 시간 한도에 도달했다는 의미입니다. Apple은 마음챙김 세션 한도를 1시간으로 문서화합니다.3 Return의 가장 긴 명상 시간은 60분이며, 이는 문서화된 상한입니다. 90분 명상은 단일 마음챙김 세션 안에서 완료될 수 없습니다: 타이머가 1시간 지점에서 세션 중간에 죽을 것입니다. 제품 결정은 시스템 관용에 도박하지 않고, 런타임 모델이 전달하도록 문서화된 범위로 사용 가능한 시간을 제한하는 것입니다.

resignedFrontmost는 사용자가 다른 Watch 앱을 열었고 여러분의 세션이 잃었다는 의미입니다. Watch 사용자는 다른 앱으로 스와이프한 다음 잊어버리는 데 능숙합니다. 제품 결정은 포기 시 일시 중지(상태 보존, 사용자가 돌아올 수 있음)이거나 포기 시 종료(세션 종료, 사용자에게 “일찍 멈춤” 신호 제공) 중 하나입니다. Return은 사용자가 명상 도중 전화를 받고 돌아올 수 있도록 포기 시 일시 중지를 선택합니다.

suppressedBySystem은 “워치가 뜨겁다”의 정중한 표현입니다. 열적 압력이나 낮은 배터리 상태의 watchOS 기기는 앱의 오용이 없어도 확장 런타임 세션을 취소할 수 있습니다. 세션 매니저는 이 경우를 우아하게 처리해야 합니다: 참조를 정리하고, 비차단 경고를 표면화하며, 시스템이 방금 거부한 세션을 다시 시작하려고 시도하는 상태에 들어가지 않아야 합니다.

willExpire 콜백은 세션이 만료되기 직전에 발생합니다. Apple의 샘플은 이를 “세션이 끝나기 전에 작업을 마무리하고 정리하는” 순간으로 설명합니다.3 이 콜백은 앱이 최종 상태 스냅샷을 작성하거나, 마무리 오디오 큐를 재생하거나, “세션이 곧 종료됨” UI를 표시할 수 있는 위치입니다. Return은 오늘날 콜백을 로깅만 하고 있습니다. 더 풍부한 정리(HealthKit 로그 항목, 오디오 페이드아웃)는 타이머의 리셋 및 완료 경로에서 발생하며, willExpire 윈도우에 대한 다르게 만들 것 목록에 있습니다.

다르게 만들 것

Return을 처음부터 다시 시작한다면 두 가지를 다르게 할 것입니다.

HealthKit 통합과 함께 가치가 증가하는 모든 세션에 HKWorkoutSession을 사용하세요. 명상 타이머는 mindfulnessworkout-processing의 경계에 자리합니다. 마음챙김이 v1에 적합한 선택이었던 이유는 데이터 모델이 더 단순하고 사용자 기대가 “이건 명상이지 운동이 아니다”이기 때문입니다. HKWorkoutSession은 더 세분화된 HealthKit 통합(세션 시작, 세션 종료, 세그먼트, 이벤트)을 제공하며, 데이터 누적을 위한 더 풍부한 LiveWorkoutBuilder 인터페이스를 제공합니다. 문서화된 Apple 보장이 아닌 아키텍처적 판단입니다: 가치가 상세한 세션 텔레메트리에 의존하는 앱의 경우, 워크아웃 세션 경로가 WKExtendedRuntimeSession이 처리하지 않는 구조를 다룹니다.

첫날부터 세션 상태 관찰성 표면을 추가하세요. Return의 첫 번째 버전은 세션 이벤트를 콘솔에 로깅했습니다. 두 번째 버전은 디버깅을 위해 기기 내 세션 상태 가시성을 추가했습니다. 세 번째 버전은 세션 무효화를 블랙박스로 취급하는 대신, 무언가 잘못되었을 때 세션 사유 이력을 사용자에게 표면화하는 개발자 모드 토글을 노출할 것입니다. watchOS 런타임은 불투명합니다. 디버그 표면이 이를 보완해야 합니다.

WKExtendedRuntimeSession이 잘못된 답인 경우

세션 타입이 맞지 않는 세 가지 경우입니다.

세그먼트 마커, 심박수 스트림 또는 활성 칼로리 추적이 필요한 운동. HKLiveWorkoutBuilder와 함께 HKWorkoutSession을 직접 사용하세요. 워크아웃 API는 Apple이 실제 운동(걷기 명상이나 격렬한 활동 포함)에 대해 문서화한 경로입니다. WKExtendedRuntimeSession은 마음챙김이나 알람과 같은 비운동 세션에 대한 문서화된 경로입니다. 명상 앱은 운동이 필요하지 않지만, Couch-to-5K 앱은 필요합니다.

Now Playing 표면이 필요한 오디오 재생. watchOS 오디오 세션 권한과 함께 재생용으로 구성된 AVAudioSession을 사용하세요. Now Playing 통합과 시스템 재생 표면이 오디오 앱이 원하는 것이며, 오디오 경로는 WKExtendedRuntimeSession과는 완전히 분리되어 있습니다. WKExtendedRuntimeSession은 Now Playing이나 시스템 오디오 라우팅을 제공하지 않습니다.

사용자 인지 없이 진행되는 장시간 데이터 동기화. 시스템이 스케줄링하는 주기적 새로 고침 윈도우에는 WKApplicationRefreshBackgroundTask를 사용하세요. 사용자는 앱 안에 있지 않습니다. 앱은 계속 실행될 필요가 없습니다. 잠시 깨어나서 새로 고침하면 됩니다. 두 가지 백그라운드 작업과 확장 런타임 세션 모델은 매우 다른 요구를 충족합니다.

이 패턴이 watchOS 11+에 출시하는 앱에 의미하는 것

세 가지 핵심 사항입니다.

  1. Watch 런타임 모델은 옵트인입니다. 세션 타입을 선택하고 그 규칙 안에서 살아가세요. watchOS에서 “일반 백그라운드 작업”을 시도하는 앱은 잃을 것입니다. mindfulness, workout-processing, self-care, physical-therapy, alarm, 또는 underwater-depth를 선택하고, 선택한 세션 타입과 함께 오는 런타임 예산을 중심으로 사용자 경험을 설계하세요.

  2. 세션 델리게이트는 앱 범위에 살아 있어야 합니다. SwiftUI의 뷰 라이프사이클은 오래 사는 상태 객체를 보호하지 않습니다. @main App 레벨에 바인딩된 static let shared 싱글톤이 내비게이션 푸시, 뷰 교체, SwiftUI의 일반적인 해제 동작에서 살아남는 가장 작은 패턴입니다.

  3. 실제 하드웨어에서 테스트하세요. 시뮬레이터는 손목 내림 런타임 모델을 강제하지 않습니다. Watch 앱이 시뮬레이터에서 테스트할 수 없는 버그가 바로 사용자에게 출시되는 버그입니다.

이 글을 동일한 앱군에 대한 이전 글들과 함께 읽으시기 바랍니다: 크로스 플랫폼 SwiftUI 출시 (Return은 iPhone, iPad, Watch, Mac, Apple TV에 출시됨); Live Activities 상태 머신 (동일한 타이머의 iOS 측 표면); HealthKit 패턴 (Watch의 마음챙김 세션이 사용자 Health 데이터에 자리 잡는 곳). 전체 세트는 Apple Ecosystem Series 허브에 있습니다. 더 광범위한 AI 에이전트와 함께하는 iOS 컨텍스트에 대해서는 iOS Agent Development 가이드를 참조하세요.

FAQ

watchOS 확장 런타임 세션이란 무엇인가요?

watchOS 확장 런타임 세션(WKExtendedRuntimeSession)은 Watch 앱이 사용자가 손목을 내린 후에도 계속 실행하기 위해 사용하는 API입니다. 세션은 Info.plistWKBackgroundModes를 통해 타입(mindfulness, workout-processing, alarm 등)을 선언해야 합니다. 활성 확장 세션이 없으면 watchOS는 손목을 내린 직후 곧 앱을 일시 중지합니다.

사용자가 손목을 내릴 때 watchOS 타이머가 카운팅을 멈추는 이유는 무엇인가요?

지원되는 타입의 활성 WKExtendedRuntimeSession이 실행 중이지 않은 한, Watch 앱은 손목을 내린 직후 곧 일시 중지됩니다. 그러한 세션을 시작하지 않는 타이머 매니저는 백그라운드 런타임이 차단되는 것을 보게 되며, 타이머 상태는 사용자가 다시 손목을 들어 올릴 때까지 손목을 내린 순간에 동결됩니다.

WKExtendedRuntimeSessionHKWorkoutSession의 차이점은 무엇인가요?

WKExtendedRuntimeSession은 마음챙김, 알람, 자기관리와 같은 비운동 세션을 위한 범용 확장 런타임 API입니다. HKWorkoutSession은 실제 운동을 위한 API입니다. HealthKit과 통합되며, 세그먼트 마커를 지원하고, 걷기 명상이나 격렬한 활동을 위한 문서화된 경로입니다. 운동 등급의 텔레메트리가 없는 마음챙김 앱은 첫 번째를 사용하고, 운동 앱은 두 번째를 사용합니다.

시스템이 내 확장 런타임 세션을 취소할 수 있나요?

예. WKExtendedRuntimeSessionInvalidationReason에는 expired(시스템 시간 한도 도달), resignedFrontmost(다른 Watch 앱이 포그라운드가 됨), suppressedBySystem(저전력 또는 열적 압력)이 포함됩니다. 세션 매니저는 각각을 깔끔하게 처리해야 합니다: 참조가 정리되고, 타이머 상태가 적절히 반응하며, 다음 세션 시작 호출이 올바르게 작동해야 합니다.

SwiftUI watchOS 앱에서 세션 매니저는 어디에 살아 있어야 하나요?

@main App 구조체에서 바인딩된 싱글톤으로 앱 범위에 살아 있어야 합니다. SwiftUI의 뷰 범위 상태(@State, @StateObject)는 내비게이션 푸시, 뷰 교체 또는 앱 백그라운드 시 해제됩니다. 세션 중간에 해제되는 뷰 소유 세션 델리게이트는 세션 참조가 누수되게 하고 후속 세션이 깔끔하게 시작되는 것을 막습니다.

참고 문헌


  1. 저자의 Return, 2026년 4월 21일 App Store에 출시된 SwiftUI 명상 타이머. iPhone, iPad, Mac, Apple Watch, Apple TV에서 사용 가능. Watch 앱은 사이클 타이머 런타임을 위해 mindfulness 백그라운드 모드와 함께 WKExtendedRuntimeSession을 사용합니다. 

  2. Apple Developer, “About the background execution sequence”. iOS 측 백그라운드 런타임 어포던스(오디오 세션, 위치, BGTaskScheduler) 및 watchOS와의 차이점. 

  3. Apple Developer, “WKExtendedRuntimeSession”. 세션 타입, 라이프사이클, 델리게이트 콜백, 런타임 한도, 그리고 WKBackgroundModes Info.plist 키. 

  4. Apple Developer, “Information Property List: WKBackgroundModes”. 지원되는 세션 타입 문자열: workout-processing, mindfulness, self-care, physical-therapy, alarm, underwater-depth

  5. Apple Developer, “Building a watchOS app” 및 WatchKit 테스팅 지침. 실제 기기 런타임 동작은 watchOS 시뮬레이터에서 재현할 수 없습니다. 시뮬레이터는 손목 내림 일시 중지를 강제하지 않습니다. 

  6. Apple Developer, “WKExtendedRuntimeSessionInvalidationReason”. 열거형 케이스: none, sessionInProgress, expired, resignedFrontmost, suppressedBySystem, error

관련 게시물

iOS 26의 HealthKit + SwiftUI: 권한 요청, 샘플 타입, 그리고 두 개의 앱을 출시하며 얻은 크로스 플랫폼 패턴

Water(물 섭취량 추적, HKQuantitySample)와 Return(마음챙김 세션, HKCategorySample)에서 가져온 실제 프로덕션 패턴. 권한 UX, async 래퍼, watchOS 변형, 그리고 …

11 분 소요

SwiftUI는 무엇으로 이루어져 있는가

SwiftUI는 값 타입 View 트리 위에 얹힌 result-builder DSL입니다. 기반 구조가 보이는 순간, AnyView, Group, ViewBuilder는 더 이상 신비로운 존재가 아닙니다.

11 분 소요

클린업 레이어가 진짜 AI 에이전트 시장이다

Charlie Labs는 에이전트를 만드는 일에서 그 뒷정리를 하는 일로 피벗했습니다. AI 에이전트 시장은 생성에서 검증으로 이동하고 있습니다. 클린업이 지속 가능한 레이어입니다.

11 분 소요