WatchKit
고친 사람 github-actions[bot]
WatchKit 은 Apple Watch 앱이 시계의 운영체제와 이야기하게 해 주는 Apple 의 도구입니다. 앱은 이 도구로 자기가 켜지고 멈추는 때를 전해 듣습니다. 손목에 진동을 보낼 때도 이 도구를 씁니다. 화면이 꺼진 동안 잠깐 깨어나 일할 때도 이 도구를 거칩니다.
쉽고 빠른 이해
WatchKit 은 시계 앱과 시계 운영체제 사이의 창구입니다. 걷기 기록 앱이 목표 걸음 수를 채웠을 때 손목에 짧은 진동을 보내는 것이 이 창구를 쓰는 일입니다.
시계는 배터리가 작습니다. 그래서 시계 운영체제는 앱이 화면 밖에서 마음대로 돌지 못하게 막습니다. 앱이 뒤에서 일하려면 운영체제에 요청해야 합니다. 그 요청을 넣는 통로가 WatchKit 입니다.
어떻게 도는가:
- 앱이 켜지거나 손목을 내려 뒤로 물러나면 운영체제가 앱에 알립니다
- 앱은 다음에 깨어나고 싶은 시각을 운영체제에 요청해 둡니다
- 그 시각 즈음 운영체제가 앱을 잠깐 깨웁니다. 앱은 할 일을 마친 뒤 끝났다고 알립니다
대가도 있습니다. 앱이 깨어나는 때는 운영체제가 정합니다. 요청한 시각보다 늦게 깨어날 수 있습니다. 그래서 시계 앱은 늘 최신 값을 들고 있다고 기대할 수 없습니다.
요즘 시계 앱의 화면은 SwiftUI 라는 Apple 의 화면 프레임워크가 그립니다. WatchKit 은 화면 밖의 일을 맡습니다.
상세
WatchKit 은 watchOS 앱을 만들 때 쓰는 Apple 의 프레임워크입니다. watchOS 는 Apple Watch 에서 도는 운영체제입니다. 프레임워크는 앱이 그 위에 얹혀 도는 코드 묶음입니다. 개발자 코드는 프레임워크가 정한 때에 불립니다.
WatchKit 은 Apple 이 내놓는 SDK(Software Development Kit, 소프트웨어 개발 키트)에 들어 있습니다. SDK 는 한 플랫폼용 앱을 짜는 데 필요한 라이브러리와 도구를 한데 묶은 꾸러미입니다. 그래서 WatchKit 은 따로 받지 않습니다. WatchKit 의 판은 watchOS 의 판을 따라 오릅니다.
Xcode 는 Apple 기기용 앱을 짜는 개발 도구입니다. Xcode 에서 시계 앱을 만들면 WatchKit 을 바로 씁니다.
이 절은 WatchKit 이 왜 따로 있는지에서 출발합니다. 그다음 WatchKit 의 클래스가 맡는 일을 하나씩 봅니다. 끝에서는 지금 시계 앱이 언제 WatchKit 을 쓰는지 가립니다.
시계가 앱을 붙잡아 두는 까닭
시계는 배터리가 작습니다. 화면도 작습니다. 사람은 손목을 들어 몇 초 보고 바로 내립니다. 그래서 watchOS 는 앱이 화면 밖에서 마음대로 돌지 못하게 합니다. 손목을 내리면 앱은 곧 뒤로 물러나고 멈춥니다.
백엔드 서버와 견주면 차이가 보입니다. 서버 프로세스는 한 번 띄우면 요청이 없어도 계속 떠 있습니다. 시계 앱은 거꾸로입니다. 운영체제가 필요하다고 볼 때만 돕니다.
이 규칙 아래서 앱이 일을 하려면 운영체제와 약속을 맺어야 합니다. 「지금 켜졌다」와 「곧 멈춘다」를 전해 듣습니다. 「이때 깨워 달라」고 요청합니다. 이 약속을 코드로 담은 것이 WatchKit 입니다.
클래스 이름의 머리글자
WatchKit 의 클래스는 이름이 WK(WatchKit) 로 시작합니다. 프레임워크 이름의 머리글자를 딴 것입니다. 이 관례는 Objective-C 시절에 굳었습니다. Objective-C 는 지금 쓰는 언어인 Swift 가 나오기 전에 Apple 앱을 짜던 언어입니다.
Objective-C 에는 네임스페이스가 없었습니다. 네임스페이스(이름 공간)는 같은 이름이 부딪히지 않게 이름을 소속별로 갈라 두는 공간입니다. 그래서 클래스 이름 앞에 머리글자를 붙여 소속을 밝혔습니다. Java 에서 패키지 이름이 하는 일을 머리글자가 대신합니다.
앱 객체와 생명주기
시계 앱 하나에는 앱 전체를 대표하는 객체가 하나 있습니다. WatchKit 에서는 WKApplication 입니다.
코드 어디서든 WKApplication.shared() 로 꺼내 씁니다. 예전 이름은 WKExtension 입니다.
이렇게 프로세스에 하나만 두고 어디서나 꺼내 쓰는 객체가 싱글턴입니다. 스프링 컨테이너가 빈을 기본으로 하나만 만들어 나눠 주는 것과 닮았습니다.
앱이 켜지고 물러나고 멈추는 흐름을 앱의 생명주기라고 부릅니다. 시계 앱의 생명주기는 손목의 움직임을 따라갑니다.
시계 앱은 네 상태를 오갑니다. 활성은 화면 앞에 나와 터치를 받는 상태입니다. 비활성은 아직 화면에 남았지만 입력을 받지 않는 상태입니다. 백그라운드에서는 화면 없이 코드만 잠깐 돕니다. 정지는 메모리에 남아 있지만 코드가 한 줄도 돌지 않는 상태입니다.
정지된 앱은 운영체제가 메모리를 돌려받으려고 끝낼 수도 있습니다. 아래 그림은 이 상태들과 그 사이를 옮기는 계기를 보입니다.
stateDiagram-v2
[*] --> 활성: 앱을 연다
활성 --> 비활성: 손목을 내린다
비활성 --> 활성: 손목을 다시 든다
비활성 --> 백그라운드: 조금 지난다
백그라운드 --> 정지: 할 일이 없다
정지 --> 백그라운드: 운영체제가 깨운다
백그라운드 --> 활성: 앱을 다시 연다
정지 --> 끝남: 운영체제가 메모리를 회수한다
끝남 --> [*]
손목을 내리는 것만으로 앱은 활성에서 비활성으로, 이어 백그라운드로 내려갑니다.
상태가 바뀌면 운영체제가 앱에 알립니다. 알림은 앱이 미리 맡겨 둔 객체가 받습니다. 「이 일이 생기면 이 객체의 메서드를 불러 달라」고 맡겨 두는 방식을 델리게이트 패턴이라고 부릅니다. Java 의 리스너 인터페이스와 같은 구실입니다.
이 객체가 따라야 할 약속은 WKApplicationDelegate 프로토콜이 정합니다. 프로토콜은 Swift 에서 Java 의 인터페이스에 해당하는 문법입니다.
아래 표는 그 프로토콜의 메서드 가운데 생명주기를 따라 불리는 넷입니다.
| 메서드 | 불리는 때 |
|---|---|
applicationDidFinishLaunching() |
앱이 처음 뜬 직후 |
applicationDidBecomeActive() |
화면 앞에 나와 입력을 받기 시작할 때 |
applicationWillResignActive() |
손목을 내려 화면 앞에서 물러나기 직전 |
applicationDidEnterBackground() |
백그라운드로 내려간 뒤 |
표에서 보듯 메서드 이름이 곧 불리는 때입니다. 물러나기 직전에 불리는 메서드에서는 쓰던 값을 저장해 둡니다. 정지된 앱은 운영체제가 알리지 않고 끝낼 수 있기 때문입니다.
기기 정보와 진동
WKInterfaceDevice 는 지금 앱이 도는 시계 한 대를 나타냅니다. 운영체제 이름, 기기 모델, 화면 크기 같은 값을 줍니다.
이 객체도 하나뿐이라 current() 로 꺼냅니다.
손목에 진동으로 신호를 주는 것이 햅틱 피드백입니다. 시계는 화면을 오래 보지 않는 기기입니다. 진동은 손목을 들지 않은 사람에게도 닿습니다.
이 진동도 WKInterfaceDevice 가 보냅니다. 아래 코드는 기기 값 둘을 읽고 진동을 한 번 보냅니다.
let device = WKInterfaceDevice.current()
device.systemName // "watchOS"
device.model // "Apple Watch"
device.play(.success) // 짧은 진동
play 에 넘기는 값은 진동의 종류입니다. .success 말고도 .failure·.click·.notification 같은 종류가 있습니다.
종류마다 떨림의 길이와 박자가 다릅니다. 앱은 떨림을 직접 짜지 않고 이 가운데서 고릅니다.
뒤에서 깨어나는 일
앱이 화면 앞에 없을 때 운영체제가 잠깐 깨워 일을 시키는 것을 백그라운드 갱신이라고 부릅니다. 날씨 앱이 손목을 들기 전에 새 기온을 받아 두려면 이것이 필요합니다. 이 소절은 그 갱신을 요청하고 받는 순서를 봅니다.
앱은 먼저 WKApplication 의 scheduleBackgroundRefresh 메서드로 깨어나고 싶은 시각을 적어 둡니다. 그리고 멈춥니다.
그 시각 즈음 운영체제가 앱을 깨워 델리게이트의 handle 메서드를 부릅니다. 할 일은 작업 객체에 담겨 넘어옵니다.
앱은 일을 마치면 작업 객체의 setTaskCompletedWithSnapshot 메서드를 불러 끝났다고 알립니다. 이름의 Snapshot 은 앱 전환 화면에 보일 앱 모습입니다.
끝났다고 알릴 때 그 모습을 새로 찍을지도 함께 알립니다.
깨어난 앱이 쓸 수 있는 시간에는 한도가 있습니다. 끝났다고 알려야 운영체제가 앱을 다시 재웁니다. 알리지 않으면 앱이 그 시간을 다 써 버립니다. 운영체제가 앱을 끝낼 수도 있습니다.
sequenceDiagram
participant 앱
participant OS as watchOS
앱->>OS: scheduleBackgroundRefresh · 바라는 시각
Note over 앱: 앱은 멈춘다
OS->>앱: handle · 작업 객체를 넘긴다
Note over 앱: 새 값을 받아 온다
앱->>OS: setTaskCompletedWithSnapshot
Note over 앱: 다시 멈춘다
이 흐름은 cron 과 닮았지만 다릅니다. cron 은 적어 둔 시각에 작업을 돌립니다. 백그라운드 갱신의 시각은 바람일 뿐입니다. 운영체제는 배터리를 아끼려고 깨우는 횟수를 아끼고 시각을 미룹니다.
앱을 깨우는 까닭은 갱신 하나만이 아닙니다. 아래 표는 handle 로 넘어오는 작업 객체의 대표 종류입니다.
| 작업 객체 | 넘어오는 때 |
|---|---|
WKApplicationRefreshBackgroundTask |
앱이 요청해 둔 시각이 됐을 때 |
WKURLSessionRefreshBackgroundTask |
뒤에서 걸어 둔 내려받기가 끝났을 때 |
WKWatchConnectivityRefreshBackgroundTask |
짝지은 iPhone 에서 데이터가 왔을 때 |
WKSnapshotRefreshBackgroundTask |
앱 전환 화면에 보일 앱 모습을 새로 찍을 때 |
표의 넷은 이름만 다릅니다. 다루는 법은 같습니다. 받아서 일을 마친 뒤 끝났다고 알립니다.
화면이 꺼져도 이어 도는 세션
몇몇 일은 잠깐 깨어나는 것으로 모자랍니다. 명상 안내나 양치 시간 재기는 몇 분 동안 끊기지 않고 돌아야 합니다.
이럴 때 앱은 WKExtendedRuntimeSession 을 엽니다. 손목을 내려도 세션이 열린 동안은 앱이 계속 돕니다.
세션은 아무 앱이나 열 수 없습니다. 명상, 자기 관리, 물리 치료, 알람처럼 운영체제가 정해 둔 종류 가운데 하나여야 합니다. 운동 기록은 이 세션이 아니라 HealthKit 의 운동 세션이 맡습니다. HealthKit 은 걸음 수와 심박 같은 건강 기록을 다루는 Apple 의 프레임워크입니다.
옛 화면 방식 — 인터페이스 컨트롤러
처음에는 시계 앱의 화면도 WatchKit 으로 짰습니다. 화면 배치는 Xcode 의 화면 편집기에서 끌어다 놓아 그렸습니다. 이렇게 그린 배치를 담는 파일이 스토리보드입니다.
화면 한 장은 WKInterfaceController 를 상속한 클래스가 맡았습니다. 이 클래스는 화면 한 장에 무엇을 보일지 정하고 버튼 입력을 받습니다.
iPhone 앱에서 같은 일을 하는 클래스는 뷰 컨트롤러입니다.
화면 위의 글자와 버튼은 WKInterfaceLabel·WKInterfaceButton 같은 인터페이스 객체로 다뤘습니다.
인터페이스 객체는 화면 부품을 직접 쥔 객체가 아닙니다. 화면 부품에 값을 보내기만 하는 대리 객체입니다.
그래서 값을 넣는 메서드만 있고 읽는 메서드가 없습니다.
label.setText("3,200") // 화면: 3,200
label.setTextColor(.red) // 글자: 빨강
첫 Apple Watch 앱은 코드가 짝지은 iPhone 에서 돌았습니다. 시계는 화면만 그렸습니다. 값이 한쪽으로만 흐르는 이 구조가 인터페이스 객체의 모양을 정했습니다. 나중에 코드도 시계 안으로 옮겨 왔습니다. 인터페이스 객체의 모양은 그 뒤에도 남았습니다.
지금은 시계 앱의 화면을 SwiftUI 로 짭니다. SwiftUI 는 화면이 어떤 모습이어야 하는지를 코드로 적으면 Apple 이 그려 주는 화면 프레임워크입니다. 스토리보드와 인터페이스 컨트롤러는 예전에 만든 앱을 고칠 때 만납니다.
WatchKit 을 쓰는 경우
지금 WatchKit 은 화면보다 화면 밖의 일을 맡습니다. 아래 표는 시계 앱에서 하고 싶은 일마다 무엇을 쓰는지 가릅니다.
| 하고 싶은 일 | 쓰는 것 |
|---|---|
| 화면을 짠다 | SwiftUI |
| 켜짐과 멈춤을 전해 듣는다 · 뒤에서 깨어난다 · 진동을 보낸다 | WatchKit |
| iPhone 앱과 값을 주고받는다 | Watch Connectivity |
| 운동하는 동안 계속 돈다 | HealthKit 운동 세션 |
| 시계 화면에 작은 정보를 띄운다 | WidgetKit |
표의 SwiftUI·Watch Connectivity·WidgetKit 은 이름은 다른 프레임워크여도 WatchKit 과 이어지는 일이 많습니다. 어떻게 이어지는지는 아래 「맞물림」 절에서 봅니다.
맞물림
시계 앱 하나는 WatchKit 혼자 만들지 않습니다. 화면은 SwiftUI 가 그립니다. iPhone 과의 통신은 Watch Connectivity 가 맡습니다. 시계 화면의 작은 정보는 WidgetKit 이 그립니다. 이 절은 그 셋이 WatchKit 에 어떻게 붙는지를 봅니다.
SwiftUI 앱에 델리게이트를 붙이는 WKApplicationDelegateAdaptor
SwiftUI 로 짠 앱은 앱의 시작점을 App 프로토콜을 따르는 구조체로 적습니다. 이 구조체에는 WatchKit 의 델리게이트가 없습니다.
생명주기 알림이나 백그라운드 작업을 받으려면 델리게이트를 따로 붙여야 합니다.
붙이는 수단이 @WKApplicationDelegateAdaptor 입니다. 속성 앞에 이 표시를 달면 SwiftUI 가 델리게이트 객체를 만들어 WKApplication 에 걸어 줍니다.
이렇게 속성에 붙여 동작을 덧입히는 Swift 문법을 프로퍼티 래퍼라고 부릅니다.
@main
struct WalkApp: App {
@WKApplicationDelegateAdaptor
var delegate: AppDelegate
var body: some Scene {
WindowGroup { ContentView() }
}
}
AppDelegate 는 앞에서 본 WKApplicationDelegate 를 따르는 클래스입니다. @main 은 이 구조체가 앱의 시작점이라는 표시입니다. Java 의 main 메서드와 같은 구실입니다.
대가는 통로가 둘이 된다는 것입니다. SwiftUI 도 scenePhase 라는 값으로 활성·비활성·백그라운드를 알려 줍니다.
같은 변화를 두 곳에서 받을 수 있습니다. 그래서 어느 쪽에서 처리할지 하나로 정해 둬야 합니다. 안 그러면 같은 저장을 두 번 하게 됩니다.
반대 방향도 있습니다. 예전에 스토리보드로 짠 앱에 SwiftUI 화면을 한 장 넣을 때는 WKHostingController 를 씁니다. SwiftUI 화면을 품는 인터페이스 컨트롤러입니다.
iPhone 앱과 값을 주고받는 Watch Connectivity
시계 앱과 짝지은 iPhone 앱은 서로 다른 기기에서 도는 두 앱입니다. 둘 사이에 값을 나르는 일은 WatchKit 이 아니라 Watch Connectivity 프레임워크가 합니다.
양쪽 앱이 각자 WCSession 을 켜면 통로가 열립니다.
값을 보내는 방법은 셋입니다. 세 방법은 두 가지에서 갈립니다. 상대가 지금 돌고 있어야 하는가, 그리고 값이 쌓이는가 덮이는가입니다.
| 메서드 | 상대가 돌고 있어야 하나 | 값은 | 백엔드에서 닮은 것 |
|---|---|---|---|
sendMessage |
✓ | 바로 가고 답을 받는다 | RPC 호출 |
transferUserInfo |
✗ | 줄을 서서 차례로 간다 | 메시지 큐 |
updateApplicationContext |
✗ | 마지막 값 하나만 남는다 | 키 하나를 덮어쓰기 |
RPC(Remote Procedure Call, 원격 프로시저 호출)는 다른 기계의 함수를 내 함수처럼 부르는 방식입니다. sendMessage 는 상대가 깨어 있을 때만 됩니다. 상대가 멈춰 있으면 실패합니다. 그래서 줄 서는 쪽으로 바꿔 보낼 준비를 해 둡니다.
「지금 설정이 무엇인가」처럼 최신 값만 의미 있는 것은 덮어쓰는 쪽이 맞습니다. 걸음 기록처럼 하나도 빠지면 안 되는 것은 줄 서는 쪽이 맞습니다.
WatchKit 과 붙는 대목은 값이 도착한 때입니다. 시계 앱이 멈춰 있는 동안 iPhone 에서 값이 오면 운영체제가 앱을 깨웁니다. 이때 넘어오는 것이 앞 표의 WKWatchConnectivityRefreshBackgroundTask 입니다.
앱은 값을 받아 저장하고 끝났다고 알립니다. 받는 쪽 코드를 백그라운드 작업 처리와 함께 짜야 하는 까닭입니다.
시계 화면의 정보를 그리는 WidgetKit
시계 화면 한구석에 날씨나 남은 걸음 수를 작게 띄우는 것을 컴플리케이션이라고 부릅니다. 지금은 WidgetKit 으로 그립니다. 예전에는 ClockKit 이 이 일을 맡았습니다. 컴플리케이션이 떠 있어도 앱은 돌지 않습니다.
그래서 앱은 보여 줄 값을 미리 넘깁니다. 「몇 시에는 이 값, 몇 시에는 저 값」을 시각 순서로 적은 목록입니다. 이 목록을 타임라인이라고 부릅니다. 운영체제는 앱을 깨우지 않고 그 목록을 따라 화면을 바꿉니다.
값이 바뀌면 타임라인을 다시 만들어야 합니다. 여기서 WatchKit 이 붙습니다. 백그라운드 갱신으로 깨어난 앱이 새 값을 받아 옵니다. 그리고 WidgetKit 의 WidgetCenter 에 타임라인을 다시 읽어 달라고 요청합니다.
대가는 늦음입니다. 다시 읽기도 운영체제가 횟수를 아낍니다. 백그라운드 갱신도 시각이 밀립니다. 두 지연이 겹치므로 컴플리케이션의 값은 지금보다 조금 뒤처질 수 있습니다.
관련 항목
WatchKit 이 속하는 상위 분류
프레임워크 · SDK · 모바일 개발 · 웨어러블 · 사용자 인터페이스
WatchKit 앱이 도는 기기와 운영체제
Apple Watch · watchOS · iPhone · iOS
WatchKit 앱을 짜는 언어와 도구
Swift · Objective-C · Xcode · Interface Builder · 스토리보드
WatchKit 을 이루는 클래스
WKApplication · WKApplicationDelegate · WKInterfaceDevice · WKInterfaceController · WKRefreshBackgroundTask · WKExtendedRuntimeSession · WKHostingController
WatchKit 과 맞물리는 Apple 프레임워크
SwiftUI · UIKit · Watch Connectivity · WidgetKit · ClockKit · HealthKit
WatchKit 이 다루는 시계 앱의 동작
생명주기 · 백그라운드 갱신 · 햅틱 피드백 · 워치 컴플리케이션 · 확장 런타임 세션
WatchKit 코드에 깔린 설계 방식
델리게이트 패턴 · 싱글턴 패턴 · 프로퍼티 래퍼 · 선언형 프로그래밍 · 명령형 프로그래밍
WatchKit 과 같은 일을 하는 다른 플랫폼 도구
Wear OS · Jetpack Compose · Compose for Wear OS
다른 이름: 워치킷 · Watch Kit