본문 바로가기
C.W.K.
Stream
Lesson 06 of 06 · published

폰이 네이티브로 갔어

~13 min · native, ios, watchos, capture-surfaces, outbox, predicted-then-performed

Level 0표식 없는 길
0 XP0/37 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"화면은 갈아끼울 수 있다고 했지. 일주일 만에 새 화면들이 우루루 왔는데, 그 밑은 아무것도 안 움직였어."

카나리아가 날았어

트랙 1에서 이 퀘스트가 약속한 게 있어. Waystone은 엔진이고, 화면들(모바일 캡처, 데스크톱 플랜 에디터, 지도)은 하나의 canonical 상태를 읽는, 갈아끼울 수 있는 클라이언트라는 거. 2026-09-10에 그 약속이 '자세' 얘기에서 '영수증'으로 바뀌었어. 아키텍처 문서는 그 자리를 미리 이름까지 지어뒀었어. 패밀리 최초의 네이티브 카나리아. "Waystone 인터페이스 전체를 다시 구현하는 게 아니라, 좁은 Breadcrumb 클라이언트"라고. 범용 SwiftUI 타깃 하나에, 플랜 편집 없고, 브레인 피커 없고, 따로 노는 피파도 없어. 브레인과 effort는 중앙에 그대로 두고, 폰은 캡처 클라이언트야. 그리고 React 클라이언트를 웹 껍데기로 싸는 대신 진짜 네이티브로 만들었어. 아빠의 결정이었는데, WKWebView 래퍼였으면 데스크톱의 번들링이랑 지도 라이브러리 결정까지 폰에 끌고 왔을 거야. '좁다'는 게 핵심인데 말이지.

새 표면은 전부 같은 투입구에 달린 입이야

그 첫 주에 나온 건 표면들의 목록인데, 진짜 레슨은 각각이 얼마나 시시해도 되는가에 있어. 공유 익스텐션은 Safari, Photos, Books 어디서든 탭 두 번으로 Crumb를 남겨. 그것도 완성된 Crumb로 써. 자기 ULID랑 그 순간의 캡처 진실까지 전부 담아서. "뭔가 공유됐음"만 적어두면 나중에 엔진이 여행 하루를 재구성해야 하는데, 그건 두-진실 규칙이 금지하는 거거든. 그린덴발트에서 21:04에 링크를 공유하고 다음 날 아침 벵엔에서 앱을 열어도, 그 Crumb는 여전히 14일 21:04로 읽혀. Siri, Action Button, Spotlight은 App Intents로 crumb를 남기고, 제어 센터랑 잠금 화면 버튼은 일부러 조용히 캡처하는 대신 앱을 열어. 버튼은 문장을 물어볼 방법이 없거든. 한 손에 폰을 쥐었을 때 진짜 시간을 먹는 건 타이핑이 아니라 잠금 해제하고 앱 찾아 누르기야. 홈 화면 위젯은 unsynced 수를 보여줘. 그 숫자가 중요해지는 순간이, 신호 잡히는 어딘가에 서서 Waystone 생각을 전혀 안 하고 있을 그 순간이거든.

이 중에 새 파이프라인은 하나도 없어. 각자 같은 Crumb 모양을 같은 인박스에 쓰고, 같은 아웃박스를 타고, 같은 엔진에서 같은 멱등 응답을 받아. 표면은 불어나도 규율은 쪼개지지 않아.

아웃박스, 드디어 증명됐어

Local-First Outbox 레슨이 상태 기계를 가르쳤다면, 네이티브 클라이언트는 그게 증명된 자리야. 그전의 정직한 장부에는 이렇게 적혀 있었어. 스위스 Crumb 스무 개를 쌓고도 "오프라인 아웃박스 동작은 미증명". 그걸 참으로 만드는 데 결정 세 개가 필요했어. 기기가 ULID를 찍어. 예전엔 엔진이 찍었는데, 그러면 응답이 날아간 POST는 재시도할 때 Crumb를 하나 더 만드는 수밖에 없었어. 아웃박스의 전제 조건 자체가 없었던 거지. 재전송 답변은 fold가 아니라 생 op log에서 해줘. fold는 tombstone 찍힌 id를 흔적도 없이 지워버리거든. 데스크톱에서 아빠가 지운 Crumb를 폰이 아직 재전송 중이면, "이미 아는 거야"라는 답을 들어야지, 부활하면 안 돼. 그리고 드레인은 거절당한 Crumb를 주차해두고 다음으로 넘어가. 큐 전체를 세우지 않아. 브라우저에선 큐가 멈춰도 견딜 만하지만, 케이블카 안에서는 고장 난 페이로드 하나가 하루치 추억을 인질로 잡는 거야. (그 일탈은 나중에 옳다고 인정받아서 모든 앱에 대한 kit 정책이 됐어. 트랙 7 이야기의 연장이야.)

손목은 같은 논증, 더 작게

워치 앱은 좁은-클라이언트 논증을 틀릴 여지가 더 적은 곳에서 한 번 더 한 거야. 버튼 하나. 말하면 그게 Crumb야. 스트림도 없고 지도도 없고 journey 피커도 없어. 두 번째 버튼이 잠깐 있긴 했어. '다른 언어' 단축 버튼. 근데 아빠의 진짜 손목에서 죽었어. watchOS에서 언어 전환은 시스템 받아쓰기 화면 자체를 swipe up하는 제스처거든. 앱이 문을 만들어줄 필요가 없어. 시스템 제스처를 엉망으로 복제하는 버튼은, 컨트롤 하나 들어갈 자리에 없는 것보다 나빠. 워치는 테일넷에 없고 앞으로도 없을 거야. 닿을 수 있는 엔진이 없으니까 폰이 워치의 엔진이야. 한 홉 더 멀어졌을 뿐, 자세는 같아. 일단 로컬에 커밋하고, 넘기고, ack를 받아야 손을 떼. ULID 하나가 세 층을 다 같이 타고 가. 워치 페이스 컴플리케이션도 빠뜨린 게 아니라 결정이야. 버튼이고, 숫자를 안 보여줘. 페이스는 watchOS 예산으로 새로고침되지 캡처 시점에 맞춰 새로고침되는 게 아니거든. 거기 숫자를 박으면 몇 분 안에 자신 있게 낡은 숫자가 되는데, 하필 그 페이스가 사람들이 힐끗 보는 표면이야.

엔진이 안 해도 됐던 것

퀘스트 전체가 가리키던 영수증이 여기 있어. 새 클라이언트 패밀리가 통째로 왔어. 폰, 워치, 공유 시트, 인텐트, 위젯. 근데 엔진이 추가한 건 전부 '튼튼한 클라이언트가 필요로 하는 정직한 절반'이었어. 재전송에 멱등으로 답하는 기기 발급 Crumb ULID, 재전송 가능한 미디어 staging, 자기 append-only 기록으로서의 route segment. 그리고 필드 하나, source가 드디어 와이어에 실려서 네이티브 캡처랑 브라우저 캡처가 로그 끝까지 구분돼. 엔진이 만든 게 포인트야. 병렬 상태도 없고, 두 번째 싱크 프로토콜도 없고, PWA가 못 읽는 네이티브 전용 레코드도 없어. 트랙 5 계약(네트워크보다 로컬 커밋 먼저, 두 진실, 포어그라운드 전용, 지금 캡처하고 나중에 조립)은 새 화면에 맞추느라 휘지 않았어. 새 화면들이 계약에 맞췄지. "화면은 갈아끼울 수 있다"가 사주던 게 바로 이거야. 여행자가 들고 다니는 것을 바꾸면서도 엔진이 소유하는 것은 재협상 안 해도 되는 거.

Code

투입구 하나, 입은 여러 개·text
캡처 표면                쓰는 것
앱 컴포저                 Crumb (글 / 사진) + 캡처 진실
공유 익스텐션             완성된 Crumb — "나중에 마저 써"는 없음
Siri / Action Button     캡처를 여는 인텐트, 또는 받아쓴 Crumb
워치                     받아쓴 Crumb, 폰에 먼저 넘김
하이크 레코더             RouteSegment -> 멈출 때 Crumb 하나 ("6.4 km 걸음")

모든 줄 공통: 기기가 찍은 ULID, 네트워크 전에 로컬 커밋,
멱등 재전송 — "이미 아는 거야"는 에러가 아니라 답이야

External links

Exercise

아빠가 일곱 번째 캡처 표면을 원해. 앱을 열지 않고 잠금 화면에서 문장 하나를 바로 저장하는 퀵 캡처야. 트랙 5 계약 중에서 얘가 Crumb를 쓰기 전에 반드시 지켜야 하는 건 뭐고, 포기해야 하는 편의는 뭘까?
Hint
ULID는 기기가 찍고, 캡처 진실은 (다음에 열 때가 아니라) 쓰는 순간에 찍고, 네트워크보다 로컬 커밋이 먼저고, 재전송은 멱등이어야 해. 포기하는 건 컴포저의 저장-전-확인 순간이야. 그러니까 스트림이 대신 뭘 해줘야 하는지 생각해봐. 조용한 Crumb가 아니라 보이고 고칠 수 있는 Crumb.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.