"에러가 가리킨 건 이 레포 것도 아니고, 고친 적도 없고, 마지막 커밋이 몇 시간 전인 파일이었어."
그래도 가족이 쓰는 이유
가족이 공유하는 Swift 코드는 패키지 하나에 들어 있고, 열 개가 넘는 앱이 .package(path: "../SharedKit")로 가져다 써. path 의존성이면 킷을 고친 게 다음 빌드에 바로 앱에 반영돼. 태그도, 버전 올리기도, 배포 단계도 없어. 사람 둘이 쓰고 앱들이 같이 움직이는 환경에선 그 즉시성이 핵심이야. 대신 전혀 딴 문제처럼 보이는 실패가 네 가지 유형으로 따라와.
1. 다른 세션이 고치다 만 게 네 빌드를 깨뜨려
path 의존성은 커밋이 아니라 작업 트리를 가리켜. TestFlight 업로드가 테스트 단계에서 죽은 적이 있어. 앱이 한 번도 안 건드린 킷 파일 안에서 cannot find 'decode' in scope 에러가 났지. 병렬 세션이 킷을 고치는 중이었고, 아직 커밋 안 한 변경이 앱 빌드에 그대로 컴파일돼 들어간 거야. 진단은 1초면 돼. git -C ../SharedKit status --porcelain이 그 파일을 dirty로 보여줘. 제대로 된 대응은 킷이 다시 컴파일될 때까지 기다렸다가 재시도하는 거야. 남의 작업 트리를 고치거나 stash하는 건 절대 답이 아니고.
2. 아카이브가 커밋 안 된 킷 코드를 싣고 나가
깨진 빌드보다 나쁜 건 성공한 빌드야. 앱의 테스트와 아카이브 사이에 킷 세션이 파일 두 개를 고치고 세 번째를 덜 끝냈어. 아카이브는 어떤 커밋에도 없는 킷을 컴파일하려다가, 세 번째 파일이 덜 끝난 덕분에 겨우 실패했어. 수정이 다 끝난 상태였다면 아카이브는 성공했을 거고, 그 빌드 번호는 아무도 재현 못 하는 바이너리를 가리켰을 거야. 그래서 가족 업로드 파이프라인은 킷 소스가 HEAD랑 다르면 배포용 아카이브를 거부하고, 킷 커밋을 빌드 기록에 찍어둬.
3. 사용처의 빌드가 낡고, 계속 낡은 채로 있어
킷 파일에 새 타입이 생겼어. 킷 자체 테스트도 통과했고. 사용처 앱은 같은 모듈에서 다른 타입 여섯 개는 멀쩡히 가져오면서, "cannot find 'ProductManifest' in scope"로 두 번 연달아 실패했어. 사용처가 빌드해둔 모듈이 새 소스 파일보다 오래된 거였어. Package.swift부터 읽지 말고 타임스탬프 두 개를 비교해. 사용처 모듈이 킷 소스보다 먼저 만들어졌으면 낡은 거고, 사용처에서 rm -rf .build 하면 끝이야. .build 일부만 지우면 다르게 실패해. 빌드 계획이 네가 지운 걸 아직 가리키고 있거든.
4. 소스 호환 변경이 링크를 깨뜨려
public 이니셜라이저에 기본값 있는 매개변수를 추가하는 건 소스 호환이야. 호출하는 곳은 전부 그대로 컴파일돼. 근데 기본 인자는 호출하는 쪽에서 채워지니까, 함수의 맹글링된 심볼이 바뀌어. 자기 소스와 시그니처가 딱 맞는 심볼인데도, 그 타입을 만드는 사용처 두 곳이 링크 단계에서 "Undefined symbols … PippaEndpoint.init(mode:host:port:)"로 실패했어. 낡은 오브젝트 파일을 들고 있었던 거지. 사용처마다 swift package clean 하니까 소스 변경 없이 풀렸어. Swift 6.3.3에서 깨끗한 패키지 두 개로 재현해보면 사용처가 다시 컴파일되고 링크도 멀쩡해. 그러니 이건 이런 변경마다 생기는 게 아니라 낡은 빌드 상태 탓이야. 구별법은 이거야. 컴파일 에러는 파일과 줄 번호를 짚고, 이 에러는 맹글링된 심볼과 오브젝트 파일을 짚어. 에러 없애겠다고 매개변수를 되돌리진 마.