Unit Test를 준비하기 위해서 Model을 Protocol로 구현하다가 만나게 된 에러입니다. 한번 보도록 할까요?
Model을 아래와 같이 Protocol로 구현한 이후 빌드를 돌리니 아래와 같은 에러가 발생을 했습니다.
import FirebaseFirestoreSwift
import Firebase
protocol WordBook {
var id: String? { get }
var title: String { get }
var timestamp: Timestamp { get }
var closed: Bool { get }
}
struct WordBookImpl: WordBook, Identifiable, Codable, Hashable {
@DocumentID var id: String?
var title: String
let timestamp: Timestamp
private let _closed: Bool?
var closed: Bool {
if let closed = _closed { return closed }
return false
}
}
에러의 내용인 즉 viewModel.wordBooks는 [WordBook] 타입인데 WordBook protocol은 Identifiable을 채택하지 않기 때문에 ForEach 안에서 사용할 수 없다는 것입니다.
그렇다면 채택하면 되겠지요? 마침 protocol 안에 id도 구현되어 있고 말이죠. 바로 아래처럼 구현해보겠습니다.
protocol WordBook: Identifiable {
var id: String? { get }
var title: String { get }
var timestamp: Timestamp { get }
var closed: Bool { get }
}
그랬더니 이게 뭐죠? 정체 불명의 에러가 여기저기서 나타났습니다. 해석을 해보면 Protocol ‘WordBook’은 generic constraint에서만 사용할 수 있다. 왜냐하면 그것은 Self 혹은 associated type requirements를 가지고 있기 때문이다.
뭔말인지 모르겠습니다… 이 포스팅에서 명명백백하게 모든 것을 밝혀낼 수 있다면 좋겠지만 모든 것을 이해하고 설명할 수준은 아직 안 되는 것 같습니다. 일단 제가 이해한 부분만 설명하도록 하겠습니다.
직역하면 제네릭 제한이라는 뜻으로 아래 코드와 같은 쓰임을 의미합니다. 아래에서 T는 제네릭입니다. 특정 타입을 T라고 정하고 나서 class의 정의 안에서 사용하는 것이죠. 따라서 Identifiable을 채택한 WordBook은 아래처럼 generic을 “제한 (constraint)”하는 용도로만 사용할 수 있습니다.
실제로 아래와 같은 코드를 구현했을 때 이 부분에서는 에러가 발생하지 않습니다.
class SomeClass<T: WordBook> {
let someProperty: T
init(_ someProperty: T) {
self.someProperty = someProperty
}
}
associatedtype에 대한 설명은 이 포스팅으로 대체합니다. 해당 포스팅을 참고해주세요!
위 에러는 Protocol이 Identifiable을 채택했기 때문에 발생하는 것입니다. Identifiable은 아래와 같이 구현되어 있습니다. 보면 associatedtype이 정의되어 있는 것을 볼 수 있습니다. 내부에 associatedtype이 사용되고 있습니다. 따라서 Identifiable을 채택한다면 WordBook의 내부에도 associatedtype이 생기는 셈이고 결국 generic constraint의 용도 외에는 사용할 수 없게 되는 것이죠.
protocol Identifiable {
associatedtype ID: Hashable
var id: ID { get }
}
일단은 Identifiable을 채택한 이상 generic constraint의 용도 이외에는 사용할 수 없습니다. 따라서 채택을 하지 않는 방법 외에는 없습니다.
protocol WordBook {
var id: String? { get }
var title: String { get }
var timestamp: Timestamp { get }
var closed: Bool { get }
}
ForEach에는 identifiable한 객체를 요구하지만 그렇지 않고 직접 id가 될 property를 지정할 수 있습니다. 아래 처럼요. (ForEach와 id에 대한 내용은 이 포스팅을 참조해주세요!)
ForEach(viewModel.wordBooks, id: \.id) { wordBook in
HomeCell(wordBook: wordBook, dependency: dependency)
}
이렇게 구현하면 WordBook protocol이 Identifiable을 준수하지 않고도 ForEach 안에서 사용할 수 있습니다.