"iOS 코드는 컴파일조차 안 되는데, 테스트 443개가 네 번이나 따로 통과했어."
공유 코드를 두는 곳
애플 앱 가족에서 코드 공유는 두 가지 형태로 나타나. 한 제품 안에선 Mac 앱과 그 아이폰 앱이 모델, 전송 프로토콜, 클라이언트 코어를 나눠 써. 제품끼리는 앱 열두어 개가 같은 보안 강화 HTTP 전송, 문서 저장소, 아웃박스, 워치 연결을 나눠 쓰고. 가족은 두 경우 모두 복사본이 아니라 Swift 패키지로 풀어. 예를 들어 Mac 터미널 겸 에이전트 클라이언트에는 모듈이 둘 있어. 플랫폼 가드가 하나도 없는 프로토콜 모듈, 그리고 전송만 플랫폼마다 다른 클라이언트 코어 모듈이야. 전송은 Mac에선 자식 프로세스, 폰에선 집 게이트웨이로 가는 WebSocket이야. 폰 클라이언트가 생겼을 때 다시 짜지 않고 그 두 모듈을 그대로 링크했어.
가족 공유 킷은 제품이 여럿인 패키지 하나고(코어, 웹 호스트, 음성, 미디어, Mac 크롬, 워치 연결, 푸시, 서버, 서버 테스트 도우미), 앱들은 path로 가져다 써. 아주 작은 단일 파일 헬퍼 몇 개는 아직 복사본으로 나눠줘. 대신 동기화 스크립트와 사용처마다 둔 드리프트 테스트가 복사본을 바이트 단위로 똑같이 유지해. 어느 방식을 쓸지 정하는 기준은 단순해. 그 파일이 다른 킷 파일에 기대느냐야. 킷 파일은 킷에서 고치고, 사용처에선 절대 안 고쳐.
무엇이 컴파일될지는 #if가 정해
플랫폼마다 다른 코드는 #if os(macOS), #if canImport(UIKit) && !os(watchOS), #if canImport(WatchConnectivity) 같은 컴파일 조건으로 감싸. 이 조건은 실행할 때 코드를 건너뛰는 게 아니야. 지금 플랫폼에 해당하지 않는 코드는 아예 컴파일이 안 돼. 가족 킷이 스스로를 속인 지점이 바로 여기야. Mac에서 swift test는 패키지를 macOS용으로만 빌드해. 하루는 테스트 443개가 네 번이나 통과했어. 그런데 조건 안쪽의 iOS와 watchOS 코드에는 문제가 줄줄이 있었어. iOS에서 컴파일 안 되는 손목 enum, Swift 6가 거부한 델리게이트 준수, 데이터 경합으로 표시된 완료 핸들러가 있었고, watchOS엔 없는 API까지 쓰고 있었어. 마지막 건 커밋 30분 뒤에, 스위트가 통과한 채로 들어갔어.
확인하는 법은 제품마다 자기가 지원한다는 플랫폼으로 전부 빌드하는 거야. 패키지 디렉터리에서 일반 대상(generic destination)으로 xcodebuild를 돌려. -derivedDataPath를 같이 쓰면 매트릭스가 증분으로 돌아. 가족 매트릭스는 제품 아홉 개를 iOS용으로, 그중 여덟 개를 watchOS용으로, 모두 17번 빌드해. 처음엔 49초쯤, 다시 돌리면 24초쯤 걸려. 킷 자체 테스트 실행기가 이 매트릭스를 불러.
앱이 먼저, 킷은 지켜보기
앱이 기다려야 하는 공유 계층은 아무것도 출시 못 하는 공유 계층이야. 가족의 결론은 이래. 킷에 없는 기능은 앱이 바로 로컬로 구현해. 이때 앱 소유, 마이그레이션 대기라고 표시하고, 킷 복사본인 척하면 안 돼. 그러고 나서 킷을 계속 지켜봐. 킷 버전이 로컬 구현을 진짜로 대신할 수 있게 되면 그걸 가져다 쓰고, 중복을 지우고, 동작이 그대로인지 확인해. 잠깐의 중복은 괜찮아. 근데 표시 없는 중복은 안 돼. 아무도 모르는 복사본은 어떤 드리프트 검사에도 안 걸리거든.