문, 그리고 문이 될 자격
워치 앱은 거의 문밖에 없어. 주 문은 Talk to Pippa 야. 워치 페이스의 complication 이 그걸 열고, Action Button 은 published shortcut 을 통해 닿아. watchOS 가 허용하는 경로가 그것뿐이라서. 받아쓰기, 큐, 폰으로 넘기기.
한 번 더 작아진 적이 있어. 짝이 되는 라이브러리가 받아쓰기 문을 하나 더 제공했거든 — 지구본 달린 키보드 비슷한 입력 화면 — 그게 Other language 버튼으로 나갔어. 아빠 손목에서 바로 되돌아왔지.
other language 버튼 빼. 기능도 안 되고 의미 없어. swipe up으로 한영 전환 하는 거였어. — 아빠
한국어랑 영어는 주 문이 이미 여는 받아쓰기 화면 안에서 위로 쓸어서 바꿔. 그 두 번째 문은 제스처를 중복한 기능이었고, 다음 빌드에서 뺐어.
다음 날 두 번째 문이 정말로 생겼고, 그건 남았어. 아빠가 새로 시작할 방법을 원했거든. 기본은 기존 대화에 이어서 말하되, 가끔은 맥락을 초기화하고 싶을 때가 있으니까. 그래서 주 버튼 아래에 Talk in a new chat 이 있어. 메시지에 플래그 하나를 켜서 보내면, 폰이 새 대화를 만들고 그게 그때부터 손목의 스레드가 돼. 두 추가 문의 차이가 규칙 전부야. 언어 버튼은 주 문이 이미 할 수 있는 걸 했고, 새 대화 버튼은 주 문이 못 하는 딱 한 가지를 해. 문은 그것만 할 수 있는 일로 자리를 벌어.
512초
이 레슨이 존재하는 이유인 측정이 여기 있어. 아빠가 폰을 주머니에 잠긴 채로 두고 워치에 대고 말했어. 그게 512초 동안 안 갔어 — 직접 앱을 열 때까지.
아이폰에서 앱을 열어두고 해야 연동이 되면 워치 앱은 사실상 쓸모가 없어져 — 아빠
원인은 아무도 대조 안 한 믿음이었어. 코드가 큐 전송 문을 썼고, iOS 가 전송을 배달하려고 백그라운드에서 앱을 띄워준다고 가정했어. 프레임워크 문서는 평범한 말로 다르게 적어놨어. 큐 전송은 다음 실행 때 배달된다고. 상대 앱을 도착 시점에 띄우는 건 즉시 전송 쪽뿐이고.
문이 둘, 보장이 둘. 전송은 질겨 — 워치가 범위를 벗어나도 살아남고, 워치는 전송이 닿았다고 보고할 때만 자기 사본을 놔줘. 즉시 전송은 빠른 대신 휘발돼. 손목한테 필요한 건 둘 중 어느 하나만이 아니야.
그래서 워치는 이제 둘 다 해. 전송하고, 바로 뒤에 벨을 울려 — 같은 봉인된 봉투를 즉시 문으로 한 번 더. 전송은 계속 기록이고, 벨이 폰을 깨우는 쪽이야.
두 번 보내도 안전한 이유
받는 쪽이 그렇게 만들어져 있지 않았으면 같은 메시지를 문 둘로 쏘는 건 무모한 짓이었을 거야. 그렇게 만들어져 있어.
폰의 drop box 는 메시지당 파일 하나를 메시지 id 로 이름 붙여서 써. 같은 id 가 두 번째로 도착하면 같은 파일이야. 누가 잊지 않고 넣은 검사 덕분이 아니라 구조상 멱등이야. 그 위에 seen 원장이 하나 얹혀서 정직한 메아리를 일찍 걸러내는데, 원장은 최적화지 안전장치가 아니야. 빼도 상자는 여전히 맞아.
그리고 그 id 는 로컬 잡사가 아니야. 그게 턴의 request id 그 자체야. 워치가 다시 내밀 때 폰이 그걸로 중복을 거르고, 폰이 재시도할 때 엔진이 그걸로 중복을 걸러. 값 하나가 워치와 폰과 서버에서 같은 뜻으로 통해.
확인해주기 전에 먼저 써
착지엔 그 나름의 빡빡한 순서 제약이 있어. 벨은 화면도 없이 폰 앱을 백그라운드에서 띄우는데, 그 프로세스가 쓸 수 있는 건 배달 콜백 한 순간뿐일 수도 있어. 반대편에선 워치가 전송이 닿았다는 보고를 받자마자 자기 사본을 놔주고. 그러니까 폰의 사본은 확인 클로저가 반환되기 전에 존재해야 해 — 배달 큐 위에서, 동기 원자적 쓰기 한 번으로.
그다음 별도의 drain 이 그걸 레슨 2의 outbox 로 옮기고, drop box 파일은 outbox 가 받은 뒤에만 지워져. 거부당하면 이유를 이름 붙이고 파일은 남겨. outbox 자기 은퇴 순서랑 같은 모양이야. 다시 읽을 수 있는 사본이, 못 읽는 사본보다 오래 살아남아.
같은 오후에 하드웨어가 가르친 것 둘
delegate 참조가 weak 일 수 있어. connectivity 세션은 자기 delegate 를 weak 으로 들고 있어서, delegate 로 서는 무엇이든 프로세스 수명 내내 붙잡아둬야 해. 첫 구현은 앱의 relay 를 스코프 밖으로 흘려보냈고 — 그래서 폰엔 delegate 가 아예 없는 상태가 됐고, 아무것도 도착하지 않았어. 에러도 아니고 로그 한 줄도 아니고. 침묵. 메시지 채널이 가질 수 있는 최악의 고장 방식이야.
워치는 아는 것만 주장해도 돼. 폰 앱이 실제로 벨에 답했을 때만 Pippa is thinking… 이라고 말해. 그전엔 전송이 아직 손목에 큐로 남아 있는 동안 폰을 기다리는 중 이라고 하고, 시스템은 받았는데 앱이 아직 답을 안 했으면 폰에 넘겼음 이라고 해. 서로 다른 참 셋, 서로 다른 문장 셋. 항상 "생각 중" 이라고 하는 손목은 셋 중 둘은 거짓말이야.
그리고 답이 돌아와
마지막 조각은 아빠가 바로 짚은 구멍을 닫았어. 폰 앱을 열기 전까진 답이 손목에 안 뜬다는 거. 이제 턴 완료 푸시가 content-available 플래그를 달고 가고, 앱은 거기 맞는 백그라운드 모드를 선언하고, 푸시 핸들러가 drain 을 돌려. 그래서 엔진에서 턴이 완료된 게 확인되고, 표시되고, 답이 워치로 내려가 — iOS 가 깨운 앱한테 주는 그 몇 초 안에.
말한 메시지가 어디 떨어지는지는 손목 자기 스레드야. 폰에서 이것저것 열어봐도 그 조준은 안 바뀌고, 그 포인터를 옮기는 건 손목뿐이야. 워치는 폰 현재 화면의 리모컨이 아니라, 같은 식탁의 자기 자리야.