"iOS가 사주는 건 OS 기능이지, 네트워크 도달 거리가 아니야."
기록하는 곳은 하나, 보는 창은 여러 개
폰이나 Mac 클라이언트가 있는 가족 앱은 구조가 전부 같아. 엔진은 집에 있는 Mac 한 대에서 돌고, 데이터베이스와 로그, 규칙까지 기록 전부를 쥐고 있어. 네이티브 앱은 얇은 클라이언트야. 사용자가 담고, 보고, 결정하는 창구 노릇을 하고, 엔진과는 tailnet으로 이야기해. tailnet은 가족 Mac, 폰, 아이패드를 이미 묶어둔 사설 네트워크야. 엔진으로 가는 공개 경로는 없어. Tailscale Funnel도 없고, 기기 안에 데이터베이스 사본을 하나 더 두지도 않아. tailnet이 없으면 앱도 없는 거고, 이걸 억지로 우회하지 않고 그대로 받아들여.
도달 거리를 넓혀주는 게 아니라면, 네이티브 클라이언트가 보태는 건 뭘까? 가족은 앱마다 매번 따지지 않으려고 목록을 적어뒀어. 믿을 수 있는 푸시, 공유 확장, App Intents와 동작 버튼, 선언한 백그라운드 모드, 담기 도구로 쓰는 카메라, 시스템이 지우지 않는 저장소, 백그라운드 오디오, 위젯과 라이브 액티비티, 햅틱, 그리고 웹 버튼에 맡기면 안 되는 일에 쓸 네이티브 컨트롤. 이 중 어느 것도 신뢰 경계를 바꾸지 않고, 기록을 쓰는 기계도 그대로야.
클라이언트가 엔진에 닿는 법
엔진은 자기 포트로 127.0.0.1에만 바인딩하고, 네트워크 인터페이스에서 직접 듣지 않아. 그 엔진을 Tailscale Serve가 tailnet에 쌍둥이로 내놔. 하나는 같은 포트 번호로 여는 평범한 HTTP고, 다른 하나는 바로 옆 포트에서 Mac의 tailnet 이름으로 된 인증서를 단 HTTPS야. 엔진이 있는 Mac에선 클라이언트가 루프백을 쓰면 되고, 다른 곳에선 tailnet 이름으로 쌍둥이 중 하나에 접속해. 쌍둥이마다 클라이언트가 치를 값(App Transport Security)과 Swift 엔진이 치를 값(어떻게 바인딩하는지)은 다음 레슨들에서 다뤄.
쌍둥이가 나란히 붙은 포트에 있으니까, 포트를 안 적은 주소는 소리 없이 엉뚱한 문을 두드리게 돼. 그래서 공유 엔드포인트 타입은 깐깐해. 받는 건 http나 https 스킴, 호스트 하나, 사용자가 직접 친 1부터 65535 사이의 포트, 그리고 끝의 슬래시뿐이야. 붙여 넣은 경로, 쿼리, 자격 증명은 조용히 버리지 않고 이유와 함께 거절해. 루프백인지, PIN이 어느 키체인 계정에 있는지 같은 주소에 관한 질문은 전부 파싱한 호스트를 보고 대답해. 어떤 앱은 "루프백이야?"를 친 글자 그대로 판단하고 다른 메서드는 파싱한 값을 쓰는 바람에, 로컬 주소인데도 PIN을 요구한 적이 있거든.
엔진이 앱보다 옛 버전일 때
폰 앱은 애플 일정에 따라 TestFlight로 업데이트되지만, 엔진은 누군가 재시작해야 업데이트돼. 여행 일지 앱의 새 빌드에서 기록 하나가 "Method Not Allowed"에 걸려 멈췄어. 요청 자체는 멀쩡했어. 집에 있는 엔진은 새 경로가 없는 채로 며칠째 돌고 있었어. 그 엔진은 /에 GET과 HEAD만 받는 정적 파일 마운트를 두고 웹 클라이언트를 내주는데, 모르는 POST가 거기로 떨어지니까 404가 아니라 405가 돌아온 거야. 엔진을 재시작하니 해결됐어. 하지만 오래 남을 수정은 클라이언트 쪽에 들어갔어. 이제 클라이언트는 엔진이 아마 앱보다 옛 버전일 거라고 말해주고, 재시작 명령 이름도 알려줘. 그 기록은 거절된 게 아니라 다시 시도할 대상으로 다루고. 클라이언트와 엔진을 함께 바꾸면 이런 어긋남은 매번 생긴다고 생각해.