뭘 하려고 했냐면

문서 스캔 앱을 새로 시작했는데요. SwiftData로 DocumentTag 모델을 만들고, 테스트를 먼저 작성하는 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라는 타입이 있더라고요. TabViewPicker에서 선택 항목을 식별할 때 쓰는 그 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 프로젝트에서 .gitkeepMultiple commands produce '.gitkeep'.gitkeep 사용 안 함

둘 다 “이전 프로젝트에서는 문제없었는데 새 프로젝트에서 터지는” 류의 함정이에요. SwiftData와 Xcode 16이 기존 관례를 깨는 부분이 은근 있으니까, 새 프로젝트 시작할 때 한번 체크해보세요.