본문 바로가기
Swift

[Swift] `@dynamicMemberLookup과 KeyPath 그리고 WritableKeyPath<Root, Value>`

by Jimmy_iOS 2023. 11. 12.

KeyPath는 Swift의 기능 중 하나로, 타입 안전한 속성 또는 서브스크립트에 대한 참조를 나타냅니다. 이를 사용하면 속성이나 서브스크립트의 값을 타입 안전하게 저장하고 조작할 수 있습니다.

예를 들어, Person이라는 구조체를 정의하고 name이라는 속성을 가지고 있다고 가정해봅시다.

이때 Person 구조체의 인스턴스에서 name 속성에 접근하고자 한다면,

KeyPath를 사용하여 타입 안전하게 이러한 속성에 접근할 수 있습니다.

struct Person {
    let name: String
}

let person = Person(name: "jimmy")
let nameKeyPath = \Person.name

person[keyPath: nameKeyPath] // "jimmy"
person[keyPath: \.name] // "jimmy"

이 때 subscript 메서드를 활용하여 코드를 더 간결하게 줄일 수 있습니다.

구조체의 깊이를 한 층 더 늘려보겠습니다.

struct Name {
    let value: String
		let lastName: String
}

struct Person {
  var name: Name
    
    subscript(keyPath: KeyPath<Person, String>) -> String {
        self[keyPath: keyPath]
    }
}

let person = Person(name: Name(value: "jimmy", lastName: "jung"))

person[\.name.value] // "jimmy"
person[\.name.lastName] // "jung"

하지만 이렇게 KeyPath로 프로퍼티에 접근하는 방식은 직관적이지 않고 복잡해 보입니다.

이때 등장하는게 바로 @dynamicMemberLookup 인데요

@dynamicMemberLookup

이 속성을 사용하면 대괄호 대신 점(.)으로 접근할 수 있습니다.

또한, 원하는 Key와 Value의 타입을 지정하여 경로를 축약하는 것도 가능합니다.

KeyPath<Name, String>을 사용하면 Name 타입 중 String 타입에 직접 접근할 수 있습니다.

@dynamicMemberLookup
struct Person {
  var name: Name
    
    subscript(keyPath: KeyPath<Person, String>) -> String {
        self[keyPath: keyPath]
    }
    
    subscript(dynamicMember keyPath: KeyPath<Name, String>) -> String {
        name[keyPath: keyPath]
    }
}

let person = Person(name: Name(value: "jimmy", lastName: "jung"))

person[keyPath: \.name.value] // KeyPath
person.value // @dynamicMemberLookup
person.lastName // @dynamicMemberLookup

@dynamicMemberLookup은 Swift 프로그래밍 언어의 기능 중 하나로,

컴파일 타임에 명시적으로 정의되지 않은 타입의 멤버에 접근할 수 있게 해줍니다.

이를 통해 타입에 대한 사용자 정의 서브스크립트 동작을 정의할 수 있습니다.

@dynamicMemberLookup
struct Person {
    let info: [String: String]
    
    subscript(dynamicMember name: String) -> String? {
        return info[name]
    }
}

let person = Person(info: ["name": "jimmy"])
let name = person.name

Person 구조체의 속성에 이름으로 접근하기 위해 서브스크립트 메서드를 사용할 수 있습니다.

컴파일 타임에는 person.name의 타입이 명시되지 않기 때문에

컴파일 타임에서는 해당 타입을 추론할 수 없습니다.

하지만 @dynamicMemberLookup를 사용하면 subscript(dynamicMember:)를 구현해

런타임에서 dynamicMember 파라미터로 받은 값에 대한 타입을 추론할 수 있게됩니다.

KeyPath에는 기본적으로 읽기 전용입니다.

하지만 Swift에서는 쓰기도 가능한 KeyPath도 지원합니다.

WritableKeyPath<Root, Value>ReferenceWritableKeyPath<Root, Value>

KeyPath의 일종으로, 속성에 대한 쓰기 작업을 지원하는 특징을 가지고 있습니다.

WritableKeyPath<Root, Value>은 일반적인 속성에 대한 쓰기 작업을 지원합니다.

즉, 해당 KeyPath를 통해 속성 값을 읽고 쓸 수 있습니다.

ReferenceWritableKeyPath<Root, Value>는 참조 타입에 대한 쓰기 작업을 지원합니다.

이는 클래스나 참조 타입에만 적용됩니다.

ReferenceWritableKeyPath를 사용하면 속성 값을 변경할 수 있습니다.

WritableKeyPath<Root, Value>@dynamicMemberLookup 을 활용하면

다음과 같은 방식으로 사용이 가능합니다.

struct Name {
    var firstName: String
    var lastName: String
    init(firstName: String, lastName: String) {
        self.firstName = firstName
        self.lastName = lastName
    }
}

@dynamicMemberLookup
class Reference<Value> {
    private(set) var value: Value
    
    init(value: Value) {
        self.value = value
    }
    
    subscript<T>(dynamicMember keyPath: WritableKeyPath<Value, T>) -> T {
        get { value[keyPath: keyPath] }
        set { value[keyPath: keyPath] = newValue }
    }
}

let reference = Reference(value: Name(firstName: "jimmy", lastName: "jung"))
reference.firstName = "sam"
reference.lastName = "sung"

WritableKeyPath<Root, Value>을 잘 활용하면 객체의 원하는 경로에 값을 가져오거나 값을 할당할 수 있게 됩니다.


Uploaded by N2T