답변 요약
// Key Path
let person = Person(name: "Alice", age: 25)
let nameKeyPath = \Person.name // Person 객체의 `name` 프로퍼티에 대한 Key Path
let ageKeyPath = \Person.age // Person 객체의 `age` 프로퍼티에 대한 Key Path
상수나 변수에 함수를 참조로 할당할 수 있는 것처럼 프로퍼티의 위치도 참조로 할당할 수 있습니다. 프로퍼티에 직접 접근해서 값을 꺼내오는게 아니라 키패스를 사용하면 간접적으로 접근하여 특정 타입의 어떤 프로퍼티 값을 가리켜야 할지 미리 지정해두고 사용할 수 있습니다.
이를 다르게 말한다면 키패스는 프로퍼티에 대한 접근을 추상화한 타입입니다.
부가 설명
// MARK: - 프로토콜을 통한 추상화
// 프로토콜 정의
protocol Named {
var name: String { get }
}
// Person 타입이 프로토콜 구현
struct Person: Named {
var name: String
var age: Int
}
// Company 타입이 프로토콜 구현
struct Company: Named {
var name: String
var employees: Int
}
// 프로토콜을 통한 추상화
func printName(namedObject: Named) {
print("Name is: \(namedObject.name)")
}
// 객체 생성
let person = Person(name: "Alice", age: 30)
let company = Company(name: "Swift Corp", employees: 100)
// 프로토콜 기반 추상화 사용
printName(namedObject: person) // 출력: Name is: Alice
printName(namedObject: company) // 출력: Name is: Swift Corp
// MARK: - 키패스를 통한 추상화
// Person 타입 정의
struct Person {
var name: String
var age: Int
}
// Company 타입 정의
struct Company {
var name: String
var employees: Int
}
// KeyPath를 통한 추상화
func printName<T>(from object: T, keyPath: KeyPath<T, String>) {
let name = object[keyPath: keyPath]
print("Name is: \(name)")
}
// 객체 생성
let person = Person(name: "Alice", age: 30)
let company = Company(name: "Swift Corp", employees: 100)
// KeyPath 사용
let personNameKeyPath = \Person.name
let companyNameKeyPath = \Company.name
printName(from: person, keyPath: personNameKeyPath) // 출력: Name is: Alice
printName(from: company, keyPath: companyNameKeyPath) // 출력: Name is: Swift Corp
KeyPath가 함의하는 바는 프로퍼티를 "추상화"한다는 것이다. 프로토콜을 통한 추상화는 인터페이스 객체가 하나더 만들어져 복잡성이 증가할 뿐만 아니라 서로 다른 타입에 대해서 추상화가 어렵다. keyPath는 프로퍼티에 대해 객체를 하나더 만들 필요없이, 타입이 서로 다르더라도, 추상화를 가능케 해준다.
keyPath는 타입을 통해 원본 객체를 건들지 않고도 읽기/쓰기 작업 제한을 걸 수 있다. KeyPath<Root, Value>, PartialKeyPath<Root> 타입으로 선언하면 프로퍼티에 대한 읽기만 가능한 경로를 열어준다.
WritableKeyPath<Type, Value>, ReferenceWritableKeyPath<Root, Value> 타입으로 선언하면 읽기/쓰기가 모두 가능한 경로를 열어준다. 값타입과 참조타입의 특성 탓에 값의 변화가 생기면 새로운 값을 내놓기 때문에 바꾸려는 객체를 var로, 메모리주소는 같고 주소 내 값만 바꾸는 ReferenceWritableKeyPath는 바꾸려는 객체를 let으로 선언해주어야한다.
값타입과 참조타입의 특성 탓에 값의 변화가 생기면 새로운 값을 내놓기 때문에 접근하려는 본체 객체가 값타입이면 var로, 접근하려는 본체가 참조타입 객체라면 let으로 선언해주어야한다.
struct Person {
var name: String
}
var person = Person(name: "Alice")
let writableNameKeyPath: WritableKeyPath<Person, String> = \Person.name
person[keyPath: writableNameKeyPath] = "Bob" // 새로운 인스턴스가 생성됨
class Person {
var name: String
}
let person = Person(name: "Alice")
let referenceWritableNameKeyPath: ReferenceWritableKeyPath<Person, String> = \Person.name
person[keyPath: referenceWritableNameKeyPath] = "Bob" // 같은 객체가 수정됨
키패스 읽기/쓰기 모두 가능한 타입에는 WritableKeyPath<Type, Value> 와 ReferenceWritableKeyPath<Root, Value> 가 있다. 전자는 값타입에, 후자는 참조타입에 적용된다. 둘 모두 프로퍼티에 대한 경로를 참조하는 타입으로 읽기/쓰기를 모두 열어준다는 특징이 있다.
struct Person {
let name: String
let age: Int
}
let person = Person(name: "Alice", age: 30)
// PartialKeyPath 사용
let partialKeyPath: PartialKeyPath<Person> = \Person.name
// 읽기 전용 접근
let nameValue = person[keyPath: partialKeyPath] // "Alice"
print(nameValue) // 출력: Alice
// MARK: - KeyPath
struct Person {
let name: String
let age: Int
}
let person = Person(name: "Alice", age: 30)
// 읽기 전용 KeyPath 사용
let nameKeyPath: KeyPath<Person, String> = \Person.name
let ageKeyPath: KeyPath<Person, Int> = \Person.age
print("이름: \(person[keyPath: nameKeyPath])") // 출력: 이름: Alice
print("나이: \(person[keyPath: ageKeyPath])") // 출력: 나이: 30
다음으로 접근하려는 객체에 대해 read only 로 제한하는 keyPath는 KeyPath<Root, Value>, PartialKeyPath<Root> 타입이 있다. 두 타입도 객체에 대해 read only 제한을 걸어 경로를 참조해주며 전자는 값타입에 대한 경로, 후자는 참조타입에 대한 경로이다. 이때 PartialKeyPath는 readOnly와 참조타입이라는 특성 덕분에 한가지 또다른 특징이 생기는데 바로 type-erase가 가능하다는 것이다.
서로다른 타입임에도 PartialKeyPath<Root> 를 이용한다면 프로퍼티에 대한 추상화가 type에 상관없이 이루어지면서 같은 클래스 내에서 서로다른 타입의 프로퍼티에 대한 추상화를 하나의 타입으로 할 수 있게 해준다.
struct Person {
var name: String
var age: Int
}
// AnyKeyPath 컬렉션 생성
let keyPaths: [AnyKeyPath] = [\Person.name, \Person.age]
let person = Person(name: "Alice", age: 30)
// AnyKeyPath를 사용하여 프로퍼티 접근
for keyPath in keyPaths {
if let nameKeyPath = keyPath as? KeyPath<Person, String> {
print("Name: \(person[keyPath: nameKeyPath])") // 출력: Name: Alice
} else if let ageKeyPath = keyPath as? KeyPath<Person, Int> {
print("Age: \(person[keyPath: ageKeyPath])") // 출력: Age: 30
}
}
만약 keyPath 의 구체타입을 type-erased 하고 싶다면 AnyKeyPath로 선언하는 방법이 있다. 다만 읽기/쓰기 작업이 들어갈 때는 반드시 구체타입으로 다운캐스팅을 해주어야한다.
keyPaths는 Hashable 프로토콜을 채택하고 있다. 따라서 딕셔너리의 키값으로 활용할 수 있음과 동시에 기존에 하나의 타입이 키값으로 들어갔으면 keyPath를 통해 다양한 정보를 키값으로 넣을 수 있다는 것이 장점이다.
다만 한가지 단점은 키패스는 값의 변화를 옵저빙할 수 없다는 것이다. 값의 변화를 옵저빙하고 싶다면 프로토콜이나 옵저버 패턴을 적용한 KVO, 클로저 콜백을 적용한 Property Wrapper를 써야한다.
레퍼런스
Introduction to Swift Keypaths: https://www.youtube.com/watch?v=z4lTKBS0ZbQ
'Flutter' 카테고리의 다른 글
Target of URI doesn't exist: 'firebase_options.dart' 에러 (1) | 2024.07.24 |
---|---|
The plugin "cloud_firestore" requires a higher minimum iOS deployment version than your application is targeting. 에러 (1) | 2024.07.24 |
Swift 에서 Key-Value Observing 이란? (0) | 2024.06.24 |
위젯에 Key 를 사용해야하는 이유 (0) | 2024.06.12 |
| WWDC 16 | Concurrent Programming with GCD (0) | 2024.06.05 |