Identifiable, 직역하면 "식별 가능한"
근데 도대체 무엇을 식별 가능하게 할 것일까?
뭔가 느낌이.. 앞서 살펴봤던 프로토콜들처럼 커스텀 타입들에 대한 것일 거 같은데..
맞습니당!! 인스턴스가 고유한 식별자를 갖게 되는 과정을 살펴봅시다~
보통 우리가 인스턴스를 비교하고 싶을 때 Equatable를 채택하고 == 연산자를 사용해서 비교한다.
struct Computer : Equatable{
let name : String
let serialNumeber : String
let price : Int
}
let computer1 = Computer(name: "computer1", serialNumeber: "000001", price: 100)
let computer2 = Computer(name: "computer1", serialNumeber: "000001", price: 100)
print(computer1 == computer2)
( Equatable 프로토콜 개념 애매하면 보고오세욥~)
[Swift] 프로토콜 뽀개볼까 (1) | Equatable & ==, === 차이
근데 만약 가격이 달라도 같은 제품군이라고 식별하고 싶다면??
id 라는 식별자를 프로퍼티로 하나 만들고 비교해 줄 수도 있다.
struct Computer {
let id : Int
let name : String
let serialNumeber : String
let price : Int
}
let computer1 = Computer(id: 1, name: "computer1", serialNumeber: "000001", price: 100)
let computer2 = Computer(id: 1, name: "computer1", serialNumeber: "000001", price: 200)
print(computer1.id == computer2.id) //true
예를 들어
"컴퓨터가 여러 개 있는데 그 컴퓨터의 속성들(이름, 가격)등을 비교해서 같은 제품군인지 비교하는 게 아니라, id(식별값)을 이용해서 같은 제품군인지 식별하도록!" 하는 경우에 이렇게 해볼 수 있겠죠??
이렇게 직접 id 프로퍼티 추가하고 식별해 줄 수도 있지만
instance 가 stable identity를 가지도록 해서 시스템적으로 식별할 수 있게 해 주려면 Identifiable프로토콜을 사용하면 된다.
# Identifiable 프로토콜, Statble identity
Identifiable protocol을 사용하면 클래스 혹은 값 타입에 안정적인 identity을 가지도록 할 수 있습니다.
그렇다면 stable identity이란?
생성된 인스턴스들이 분명하게 서로 다른 값이라는 비교가 가능하도록 하는 프로퍼티를 의미한다고 합니다!
위에서 말한 것처럼 Equatable, ==연산자를 사용해서 인스턴스의 값을 비교하는 게 아니라, 고유 식별값(stable identity)을 활용해서 구분하고 싶을 때 Identifiable 프로토콜을 사용해 줄 수 있다는 것!!
Identifiable프로토콜은 내부적 구조를 살펴보묜
Hashable프로토콜을 준수하는 id 프로퍼티 하나만 가지는 아주 단순한 프로토콜입니다.
이쯤에서 저번에 정리했던 hashable 복습!
hashable 하다는 게 뭐였죠?
- 해시함수를 통해 해싱되어 해시값을 만들 수 있는 것.
- 기본 유형(Int, String, Bool)들은 기본적으로 hashable 하고, 커스텀 타입의 경우에는 hashable을 채택하고 필요한 경우 hash(into:)함수까지 구현해주어야 했죠.
dentifiable라는 프로토콜을 사용하면, Identifiable 프로토콜을 채택한 타입은 고유 개채를 구분하기 위해서 비교 알고리즘에 이 id(라는 프로퍼티)를 사용하게 되고 id가 다르다면 서로 다른 대상이 되는 것이며, 값이 다르더라도 id가 같으면 동일한 개체로 구분되게 되는 것입니다.
# Identifiable 사용
이제 어떻게 이 프로토콜을 준수할 수 있는지 알아볼까요?
📍구조체에서의 사용
Identifiable만 채택하면 에러가 나요
🚨Type 'Computer' does not conform to protocol 'Identifiable’
Identifiable를 채택했다면 프로토콜의 요구사항인 id 프로퍼티도 추가해주어야 한다는 거죠.
이 id는 hashable 해야 하는데 저는 Int로 지정해 주었기 때문에 Swift 기본 유형으로 Hashable 합니다!
struct Computer : Identifiable{
let id : Int
let name : String
let serialNumeber : String
let price : Int
}
📍 클래스에서의 사용
근데 클래스에서는 Identifiable 프로토콜 채택만 해주면, id 프로퍼티를 따로 만들지 않아도 구조체에서 났던 것 같은 에러가 안 나네요,,?
class Computer : Identifiable{
let name : String
let serialNumeber : String
let price : Int
init(name: String, serialNumeber: String, price: Int) {
self.name = name
self.serialNumeber = serialNumeber
self.price = price
}
}
let computer1 = Computer(name: "computer1", serialNumeber: "000001", price: 100)
let computer2 = Computer(name: "computer1", serialNumeber: "000001", price: 200)
print(computer1.id)
print(computer2.id)
print(computer1.id == computer2.id)
그리고 id 프로퍼티를 직접 만들어주지 않았음에도 불구하고 이런 식으로 id 값에 접근도 가능하고 각 인스턴스의 id 값을 비교도 할 수 있습니다.
출력 결과는 아래와 같습니다!
어떻게 이게 가능한 걸까요?? 궁금해서 다시 공식문서를 보니
"클래스 타입은 기본적으로 객체 식별자(ObjectIdentifier)에대한 구현을 제공한다"고 하네요!
그리고 그 식별자는 객체의 수명(life time)동안 고유하게 유지되는 것을 보장한다고 합니다!
((( 클래스가 웬일로 친절,..?ㅎㅎ)))
하지만 뭔가 더 강력한 식별자를 원한다면 커스텀해서 구현하는 것이 좋다고 합니다! 참고!
역시 답은 다 공식문서에 나와있군영
https://developer.apple.com/documentation/swift/identifiable
📍열거형에서의 사용
연관값 없는 열거형
앞선 Hashable 설명 글에서 말했다시피 연관값이 없는 열거형은 자동적으로 Hashable을 준수하게 된다.
그러므로 이 열거형이 Identifiable프로토콜을 따르도록 하는 방법에는 Identifiable 프로토콜을 추가하고 self를 ID로 사용할 수 있다!
(( self는 Computer의 인스턴스이고 Computer가 hashable하기 때문에 id로 사용이 가능한 것! ))
enum Computer : Identifiable{
case case1
case case2
var id: Self {
return self
}
}
연관값 있는 열거형
연관값이 없는 열거형은 자동적으로 Hashable을 준수하지 않는다!!
그렇기 때문에
1) 먼저, Hashable을 준수하게 만들고!
2) 그다음, Identifiable을 준수하도록 만들면 됩니다!
연관값이 hashable(밑에서는 String이므로 hashable하다)하면 Hashable프로토콜만 채택해 주면 Hashable을 준수하게 되는 것임으로 1)번 과정은 쉽다.
먼저 이렇게 Hashable한 열거형을 만들어주고
enum Computer : Hashable{
case case1(name:String)
case case2(name:String)
}
그리고나서 2)번 과정, 즉, Identifiable을 채택해 주고 hashable한 self를 id로 지정해 주면 되는 것!
enum Computer : Hashable, Identifiable{
case case1(name:String)
case case2(name:String)
var id: Self {
return self
}
}
# 만약, 인스턴스 생성 시마다 다른 id를 가지게 하고 싶다면!
struct Computer : Identifiable{
let id = UUID()
let name : String
let serialNumeber : String
let price : Int
}
UUID를 사용해 주면 인스턴스를 생성할 때마다 새로운 값이 식별자가 될 수 있겠죠
# 만약, name 이라는 프로퍼티를 식별자로 사용하고 싶다면!
struct Computer : Identifiable{
var id : String {name}
let name : String
let serialNumeber : String
let price : Int
}
계산 속성 getter로 이렇게 설정해 줄 수 있겠죠
# SwiftUI에서의 사용
SwiftUI를 사용하면서 Identifiable의 필요 시점을 가장 많이 겪었는데
그 이유는!
List나 ForEach처럼 데이터를 나열하는 뷰에서 각 요소들을 식별해야 하기 때문에 Identifiable 프로토콜을 적용해야 하는 것입니다~
컴파일러가 식별자로 어떤 것을 사용해야 하는지 알 수 없기 때문에
Identifier를 준수하거나 명시적으로 식별자를 알려주어야 하는 것이죠!
struct Computer {
let name : String
let serialNumeber : String
let price : Int
}
let computer1 = Computer(name: "c1", serialNumeber: "000001", price: 100)
let computer2 = Computer(name: "c1", serialNumeber: "000002", price: 200)
let computer3 = Computer(name: "c2", serialNumeber: "000003", price: 300)
let computers = [computer1, computer2, computer3]
var body: some View {
VStack {
ForEach(computers){ computer in
Text(computer.name)
}
}
}
이렇게 나열해 주는 뷰를 만들려고 하면 에러가 날 거예요
🚨 Referencing initializer 'init(_:content:)' on 'ForEach' requires that 'Computer' conform to 'Identifiable’
아까 말했던 것처럼 List나 ForEach를 사용해 주려면 Computer라는 타입 Identifiable를 준수하고 있어야 한다는 것이죠. 그럼 컴파일러가 식별자로 어떤 것을 사용해야 하는지 알게 되겠죠??
그럼 Identifiable를 채택하고 id 프로퍼티를 생성해 봅시다.
struct Computer : Identifiable {
let id : Int
let name : String
let serialNumeber : String
let price : Int
}
let computer1 = Computer(id: 1, name: "c1", serialNumeber: "000001", price: 100)
let computer2 = Computer(id: 2, name: "c1", serialNumeber: "000002", price: 200)
let computer3 = Computer(id: 3,name: "c2", serialNumeber: "000003", price: 300)
internal let computers = [computer1, computer2, computer3]
이렇게 해주니 ForEach에서의 에러가 없어졌습니다 ㅎㅎ
근데 Identifiable를 직접 채택해 주는 방법 이외에도 인스턴스 자체를 hash 할 수 있게 만들어서 식별값처럼 사용해 줄 수 있습니다.
struct Computer : Hashable{
let name : String
let serialNumeber : String
let price : Int
}
let computer1 = Computer(name: "c1", serialNumeber: "000001", price: 100)
let computer2 = Computer(name: "c1", serialNumeber: "000002", price: 200)
let computer3 = Computer(name: "c2", serialNumeber: "000003", price: 300)
internal let computers = [computer1, computer2, computer3]
var body: some View {
VStack {
ForEach(computers, id : \.self){ computer in
Text(computer.name)
}
}
}
이렇게요!!
ForEach에 id라는 인자를 넘겨주는데 \.self를 사용함으로써 각각의 인스턴스 자체를 id, 즉, 식별자로 사용하겠다는 의미입니다.
그리고 아까 id는 hashable 해야 한다고 했으니!! 인스턴스 자체가 Hashable 하도록 Hashable프로토콜도 채택해주어야 합니다!
이렇게 식별자가 있어야 하는 경우를 알아보면서 Identifiable 프로토콜의 사용까지 보았습니다!
SwiftUI에서 사용만 해봤지 왜 필요한지는 잘 모르고 그냥 넘어갔었는데, 이번 정리를 계기로 Identifiable 프로토콜을 좀 더 깊이 이해하게 되었네용~
'Swift' 카테고리의 다른 글
네임스페이스를 관리해보자 | enum, struct로 namespace 관리하기 (0) | 2024.06.21 |
---|---|
[Swift] 날짜를 포맷팅하는 또다른 방법 .formatted() | DateFormatter말고 formatted 사용해보자 ( + FormatStyle) (2) | 2024.05.29 |
[Swift] 프로토토콜 뽀개볼까 (4) | Hashable, HashTable (0) | 2024.05.09 |
[Swift] 프로토콜 뽀개볼까 (3) | Codable (feat. Encodable & Decodable), JSON이란? (1) | 2024.04.28 |
[Swift] 프로토콜 뽀개볼까 (2) | Comparable (1) | 2024.04.27 |