
@dynamicMemberLookup은 Swift 4.2에서 도입된 속성(Attribute)으로, 특정 타입이 존재하지 않는 멤버(프로퍼티)에 접근하려고 할 때 컴파일러 오류를 발생시키는 대신, 사용자 정의 로직을 실행하도록 허용한다.
즉, 런타임에서 멤버 접근을 '가로채서' 처리할 수 있게 해준다.
// 예시 코드
@dynamicMemberLookup
struct JSONWrapper {
let json: [String: Any]
subscript(dynamicMember member: String) -> Any? {
return json[member]
}
}
let wrapper = JSONWrapper(["randomNumber":100])
print(wrapper.randomNumber) // 출력: 100
subscript는 Collection 프로토콜이 요구하는 함수이다.
public protocol Collection: Sequence {
//...
subscript(position: Index) -> Element { get }
}
이는 우리가 흔히 사용하는 Array와 같은 컬렉션 타입들이 준수하고 있는 프로토콜로, arr[0]과 같은 접근을 통해 해당 컬렉션 타입이 가지고 있는 멤버에 접근할 수 있게 도와주는 함수이다.
@dynamicMemberLookup은 항상 subscript(dynamicMember member: String)라는 메서드를 구현해야 하기 때문에 어떤 식으로 동작하는지 이해하기 위해서는 subscript에 대한 사전 이해가 필수이다.
KeyPath는 Swift 4에서 도입된 기능으로, 타입에 정의된 특정 프로퍼티에 대한 강력하게 타입이 지정된 참조를 생성한다.
@dynamicMemberLookup이 런타임에 문자열 기반으로 멤버에 접근하는 것과 달리, KeyPath는 컴파일 타임에 안전하게 프로퍼티를 참조한다.
KeyPath는 \(백슬래시)로 시작하며, 루트 타입과 그 뒤에 연결된 프로퍼티 이름들을 포함한다.
\Root.value\Root.property1.property2KeyPath<Root, Value>: 읽기 전용 프로퍼티 참조(가장 일반적)WritableKeyPath<Root, Value>: 읽기/쓰기가 가능한 프로퍼티 참조ReferenceWritableKeyPath<Root, Value>: 참조 타입(클래스)의 읽기/쓰기가 가능한 프로퍼티 참조주로 정렬, 필터링, UI 바인딩 등에서 활용된다.
struct Person {
var name: String
let age: Int
var address: Address
}
struct Address {
var city: String
var street: String
}
let person1 = Person(name: "Alice", age: 30, address: Address(city: "Seoul", street: "Gangnam-daero"))
let person2 = Person(name: "Bob", age: 25, address: Address(city: "Busan", street: "Haeundae-ro"))
let person3 = Person(name: "Charlie", age: 35, address: Address(city: "Seoul", street: "Teheran-ro"))
var people = [person1, person2, person3]
// 1. KeyPath를 사용한 값 접근
let nameKeyPath = \Person.name // KeyPath<Person, String>
let ageKeyPath = \Person.age // KeyPath<Person, Int> (읽기 전용)
let cityKeyPath = \Person.address.city // KeyPath<Person, String>
print(people[0][keyPath: nameKeyPath]) // "Alice"
print(people[1][keyPath: ageKeyPath]) // 25
print(people[2][keyPath: cityKeyPath]) // "Seoul"
// 2. WritableKeyPath를 사용한 값 변경
let writableNameKeyPath = \Person.name // WritableKeyPath<Person, String>
people[0][keyPath: writableNameKeyPath] = "Alicia"
print(people[0].name) // "Alicia"
// 3. 정렬에 활용
// 나이 순으로 정렬 (오름차순)
let sortedByAge = people.sorted(by: { $0[keyPath: ageKeyPath] < $1[keyPath: ageKeyPath] })
print(sortedByAge.map { $0.name }) // ["Bob", "Alicia", "Charlie"]
// 도시 순으로 정렬 (오름차순)
let sortedByCity = people.sorted(by: { $0[keyPath: cityKeyPath] < $1[keyPath: cityKeyPath] })
print(sortedByCity.map { $0.name }) // ["Bob", "Alicia", "Charlie"] (B, S, S 순)
// Swift 5.2부터는 KeyPath를 바로 정렬 클로저에 사용할 수 있습니다.
let sortedByAgeConcise = people.sorted(by: \.age)
print(sortedByAgeConcise.map { $0.name }) // ["Bob", "Alicia", "Charlie"]
// 4. UI 프레임워크 (예: SwiftUI)에서 바인딩에 활용
// SwiftUI에서는 @Binding과 함께 KeyPath를 사용하여 뷰와 모델의 프로퍼티를 연결합니다.
// TextField("Name", text: $person.name) 에서 $person.name이 WritableKeyPath와 유사한 역할을 합니다.
@dynamicMemberLookup처럼 런타임에 임의의 문자열로 프로퍼티에 접근할 수는 없다. 즉, 프로퍼티는 컴파일 타임에 미리 정의되어 있어야 한다.@dynamicMemberLookup 속성은 subscript(dynamicMember member: String) 메서드를 정의하여 작동한다.
클래스, 구조체, 또는 열거형에 @dynamicMemberLookup을 표시하고 이 서브스크립트를 구현하면, 해당 타입의 인스턴스에서 정의되지 않은 멤버에 .dot syntax로 접근할 때 이 서브스크립트가 호출된다.
이 때, member 매개변수는 접근하려고 했던 멤버의 이름을 담고 있다.

여기서 subscript의 매개변수는 ExpressibleByStringLiteral 프로토콜을 준수하고 있거나, KeyPath인 경우에만 사용할 수 있다.
ExpressibleByStringLiteral은 문자열 리터럴(String Literal), 즉 코드에 직접 큰따옴표(")로 작성된 문자열을 사용해서 해당 타입의 인스턴스를 직접 생성할 수 있도록 해주는 프로토콜이다.
이 프로토콜을 채택하고 있는 대표적인 타입으로는 String, Character, StaticString, Selector 등이 있다.
// 예시
let string: String = "string" // 가능
let int: Int = "123" // 불가능
@dynamicMemberLookup는 주로 JSON 파싱, Wrapper 타입, 동적으로 프로퍼티에 접근해야 하는 DSL(Domain Specific Language) 등에서 유용하게 사용된다.
// 사용 예시
@dynamicMemberLookup
struct JSONWrapper {
let json: [String: Any]
// 정의되지 않은 멤버에 접근 시 호출되는 서브스크립트
// `member`는 접근하려 했던 프로퍼티 이름 (예: "name", "age")
subscript(dynamicMember member: String) -> Any? {
return json[member] // 딕셔너리에서 해당 키를 찾아 값을 반환
}
// 체이닝을 위해 자기 자신을 반환하는 서브스크립트도 구현 가능
subscript(dynamicMember member: String) -> JSONWrapper? {
if let dict = json[member] as? [String: Any] {
return JSONWrapper(json: dict)
}
return nil
}
}
let jsonString = """
{
"user": {
"name": "Alice",
"age": 30,
"contact": {
"email": "alice@example.com",
"phone": "123-4567"
}
},
"isAdmin": true
}
"""
if let jsonData = jsonString.data(using: .utf8),
let jsonObject = try? JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
let data = JSONWrapper(json: jsonObject)
// 정의되지 않은 멤버에 `.dot syntax`로 접근
let userName = data.user?.name as? String // "Alice"
let userAge = data.user?.age as? Int // 30
let userEmail = data.user?.contact?.email as? String // "alice@example.com"
let isAdmin = data.isAdmin as? Bool // true
print("User Name: \(userName ?? "N/A")")
print("User Age: \(userAge ?? -1)")
print("User Email: \(userEmail ?? "N/A")")
print("Is Admin: \(isAdmin ?? false)")
// 존재하지 않는 멤버에 접근 시 nil 반환 (Any? 타입이므로)
let nonExistent = data.user?.address as? String
print("Non-existent address: \(nonExistent ?? "N/A")")
}
dynamicMemeberLookup과 KeyPath를 결합하여 특정 유즈케이스에서 유연성을 높이면서도 일정 수준의 타입 안정성을 확보할 수 있다.
아래 예시는 dynamicMemeberLookup을 사용하여 MVVM 패턴에서 ViewModel을 구현한 것이다.
@dynamicMemberLookup
class ViewModel: ViewModelProtocol {
enum Action {
case fetchExchangeRates
}
struct State {
var exchangedRates: [ExchangedRates]
}
private(set) var state: State
func action(_ action: Action) { }
subscript<T>(dynamicMember keyPath: KeyPath<State, T>) -> T {
state[keyPath: keyPath]
}
}
// 초기화
let viewModel = ViewModel(initialState: State(exchangedRates: []))
// 액션 실행
viewModel.action(.fetchExchangeRates)
// 상태 접근 (동적 멤버 룩업 사용)
let rates = viewModel.exchangedRates // [ExchangedRates] 타입
이와 같이 구현하면 본래 viewModel.state.exchangedRates로 접근해야 할 프로퍼티를 viewModel.exchangeRates로 단축해서 사용할 수 있다.
| dynamicMemberLookup | KeyPath | |
|---|---|---|
| 목적 | 정의되지 않은 멤버에 .dot syntax로 동적 접근 허용 | 타입의 특정 프로퍼티에 대한 타입 안전 참조 생성 |
| 타입 안정성 | 런타임에 결정, 컴파일 타입 안정성 낮음(오타 감지 어려움) | 컴파일 타임에 결정, 높은 타입 안정성 |
| 접근 방식 | subscript(dynamicMember:) 호출 | [keyPath: someKeyPath] 또는 .someKeyPath |
| 사용 시점 | JSON 파싱, Wrapper, DSL 등 동적 멤버 이름이 필요한 경우 | 정렬, 필터링, UI 바인딩 등 고정된 프로퍼티 참조가 필요한 경우 |
| 자동 완성 | 미지원 | 지원 |
.dot syntax를 사용하여 마치 실제 프로퍼티처럼 접근할 수 있어 코드가 더 자연스러워 보임nil이 반환되거나 크래시가 발생할 수 있음..dot syntax로 접근하는 동적 멤버에 대해서는 Xcode가 자동 완성을 제공하지 않음.@dynamicMemberLookup을 사용하면 정의되지 않은 프로퍼티에 대해 dot 문법으로 접근할 수 있다.
특히, 객체 내에 subcript(dynamicMember:)를 구현하면, 객체를 보다 유연하게 동작하도록 만들 수가 있기 때문에 상황에 따라 알맞게 사용하면 유연한 코드를 작성할 수 있다.
단일로 사용할 경우 컴파일 타입 안정성이 낮지만, KeyPath와 함께 사용하면 타입 안정성을 확보할 수 있다.