뭘 하려고 했냐면
문서 스캔 앱을 새로 시작했는데요. SwiftData로 Document와 Tag 모델을 만들고, 테스트를 먼저 작성하는 TDD로 진행하고 있었어요.
모델 코드는 심플했거든요:
@Model
class Tag {
var name: String
var color: String
@Relationship(inverse: \Document.tags)
var documents: [Document]
}
여기까지는 문제없었어요. 빌드도 잘 되고.
첫 번째 삽질: Tag가 내 Tag가 아닌데요
테스트에서 FetchDescriptor를 쓰는 순간 터졌어요:
let fetched = try context.fetch(FetchDescriptor<Tag>())
error: 'Tag' is ambiguous for type lookup in this context
처음엔 뭔 소린가 했는데요. SwiftUI에 이미 Tag라는 타입이 있더라고요. TabView나 Picker에서 선택 항목을 식별할 때 쓰는 그 Tag요.
테스트 파일에서 @testable import scansort를 하면 내 모듈의 Tag와 SwiftUI의 Tag가 동시에 스코프에 들어오면서 컴파일러가 어떤 Tag인지 판단을 못 하는 거예요.
임시로 해본 것들
처음엔 scansort.Tag로 모듈명을 명시해봤어요:
let fetched = try context.fetch(FetchDescriptor<scansort.Tag>())
컴파일은 됐는데, 테스트 파일 곳곳에서 Tag를 쓸 때마다 scansort.Tag를 써야 해서 지저분해지더라고요.
typealias도 시도했는데:
typealias ScanSortTag = scansort.Tag
이걸 두 개의 테스트 파일에 각각 넣었더니 같은 테스트 모듈 안이라 invalid redeclaration 에러가 나요.
해결: 그냥 이름을 바꿨어요
결국 모델명 자체를 DocumentTag로 변경했어요:
@Model
class DocumentTag {
var name: String
var color: String
@Relationship(inverse: \Document.tags)
var documents: [Document]
}
모호성이 원천적으로 사라지니까 테스트 코드가 깔끔해졌어요:
let fetched = try context.fetch(FetchDescriptor<DocumentTag>())
교훈: SwiftData 모델 이름을 지을 때 SwiftUI/Foundation 타입과 겹치지 않는지 먼저 확인해야 해요. Tag, Item, Group, Label 같은 흔한 이름은 높은 확률로 충돌합니다. 도메인 접두사를 붙이는 게 안전해요.
두 번째 삽질: .gitkeep이 빌드를 깨뜨린다고?
같은 날 또 하나 터졌는데요.
프로젝트 폴더 구조를 미리 잡아두려고 Models/, Views/, Services/ 같은 빈 디렉토리를 만들었어요. Git은 빈 폴더를 추적 안 하니까 관례대로 .gitkeep 파일을 넣었거든요:
for dir in Models Services ViewModels Views/{Inbox,Archive,Scan}; do
touch "$dir/.gitkeep"
done
빌드 돌렸더니:
error: Multiple commands produce '...scansort.app/.gitkeep'
원인: Xcode 16의 file-based 프로젝트
Xcode 16부터 새 프로젝트는 objectVersion 77이라는 새로운 프로젝트 포맷을 써요. 핵심 변화가 뭐냐면, 소스 디렉토리 안의 모든 파일이 자동으로 타겟에 포함된다는 거예요.
예전 Xcode에서는 파일을 추가하면 project.pbxproj에 명시적으로 등록했거든요. 근데 objectVersion 77에서는 파일시스템 = 프로젝트 구조예요. 폴더 만들면 Xcode에 바로 보이고, 파일 넣으면 바로 빌드에 포함돼요.
그래서 .gitkeep 파일 12개가 전부 앱 번들의 리소스로 복사되면서, 이름이 다 .gitkeep으로 같으니까 Multiple commands produce 에러가 난 거예요.
해결: 그냥 지웠어요
find scansort -name ".gitkeep" -delete
Xcode 16 file-based 프로젝트에서는 .gitkeep이 필요 없어요. 빈 폴더는 Xcode가 알아서 보여주고, 실제 Swift 파일을 추가하는 순간 Git도 자연스럽게 추적하게 되니까요.
교훈: Xcode 16(objectVersion 77) 프로젝트에서는 소스 디렉토리 안에 non-Swift 파일을 함부로 넣으면 안 돼요. .gitkeep, .swiftlint.yml 같은 파일도 빌드 리소스로 포함될 수 있어요.
정리
| 함정 | 증상 | 해결 |
|---|---|---|
| SwiftData 모델명이 SwiftUI 타입과 충돌 | 'Tag' is ambiguous for type lookup | 도메인 접두사 붙이기 (DocumentTag) |
Xcode 16 file-based 프로젝트에서 .gitkeep | Multiple commands produce '.gitkeep' | .gitkeep 사용 안 함 |
둘 다 “이전 프로젝트에서는 문제없었는데 새 프로젝트에서 터지는” 류의 함정이에요. SwiftData와 Xcode 16이 기존 관례를 깨는 부분이 은근 있으니까, 새 프로젝트 시작할 때 한번 체크해보세요.