KeyPath는 Swift에서 객체의 프로퍼티에 대한 타입화된 경로이며, 일급 값으로 표현됩니다. KVC(Key-Value Coding)의 문자열 키와 달리 KeyPath는 컴파일 타임에 컴파일러에 의해 검사됩니다. 컴파일러는 루트 객체의 타입, 프로퍼티의 타입을 알고 경로가 존재함을 보장할 수 있습니다. KeyPath는 Combine에서 반응형 바인딩, SwiftUI에서 양방향 바인딩, 표준 라이브러리에서 컬렉션 정렬 및 필터링에 사용됩니다. Apple Developer, 2025에 따르면 KeyPath는 Swift에서 함수형 프로그래밍의 기본 구성 요소입니다.
주요 포인트
KeyPath는 Swift 표준 라이브러리의 타입으로, 객체의 프로퍼티에 대한 경로를 나타냅니다. 점 표기법(object.property)이나 문자열 키(value(forKey:))를 통해 수동으로 프로퍼티에 접근하는 것과 달리 KeyPath는 일급 객체입니다: 인수로 전달하거나, 변수에 저장하거나, 결합할 수 있습니다.
KeyPath는 백슬래시와 프로퍼티 경로로 작성됩니다: \Person.name. 경로는 중첩될 수 있습니다: \Person.address.city. 컴파일러는 Person에 name 프로퍼티가 있고 name이 실제로 String인지 확인합니다. 프로퍼티가 존재하지 않거나 타입이 잘못된 경우 코드는 컴파일되지 않습니다.
struct Person: Codable {
let name: String
let age: Int
var isActive: Bool
}
// KeyPath(읽기 전용)
let namePath: KeyPath<Person, String> = \Person.name
let person = Person(name: "Alice", age: 30, isActive: true)
let name = person[keyPath: namePath]
KeyPath를 통한 프로퍼티 접근은 서브스크립트 object[keyPath: path]를 통해 이루어집니다. 읽기 전용 프로퍼티의 경우 KeyPath가 사용됩니다. 프로퍼티가 가변(var)인 경우 WritableKeyPath를 사용할 수 있습니다. 참조 타입의 경우 ReferenceWritableKeyPath를 사용하면 KeyPath를 통해 직접 프로퍼티를 수정할 수 있습니다.
KeyPath에는 프로퍼티에 대한 접근 수준을 반영하는 하위 타입 계층 구조가 있습니다. 기본 계층 구조는 다음과 같습니다: AnyKeyPath → PartialKeyPath → KeyPath → WritableKeyPath → ReferenceWritableKeyPath. 각 하위 타입은 기능을 추가합니다.
KeyPath — 읽기 전용. WritableKeyPath — 값 타입(struct)의 읽기 및 쓰기. ReferenceWritableKeyPath — 참조 타입(class)의 읽기 및 쓰기. Swift는 컨텍스트에 따라 자동으로 올바른 하위 타입을 선택합니다.
| 타입 | 접근 | 객체 타입 | 변경 |
|---|---|---|---|
| KeyPath | 읽기 전용 | 모두 | 불가 |
| WritableKeyPath | 읽기 및 쓰기 | 값 타입(inout) | mutating 컨텍스트 통해 |
| ReferenceWritableKeyPath | 읽기 및 쓰기 | 참조 타입 | 직접 할당 |
// 값 타입용 WritableKeyPath
var mutablePerson = Person(name: "Bob", age: 25, isActive: false)
let writablePath: WritableKeyPath<Person, Bool> = \Person.isActive
mutablePerson[keyPath: writablePath] = true
// 클래스용 ReferenceWritableKeyPath
class User: NSObject {
@objc dynamic var name: String = ""
}
let user = User()
let refPath: ReferenceWritableKeyPath<User, String> = \User.name
user[keyPath: refPath] = "Charlie"
중요한 차이점: 값 타입의 경우 KeyPath를 통한 설정에는 var(inout 컨텍스트)가 필요하지만, 참조 타입의 경우 가변 프로퍼티만 있으면 됩니다. 이는 Swift의 일반적인 의미 체계와 일치합니다: 값 타입은 값으로 전달되므로 변경에는 컨테이너의 가변성이 필요합니다.
KeyPath는 Combine과 SwiftUI에서 중추적인 역할을 합니다. Combine에서 assign 연산자는 KeyPath를 사용하여 퍼블리셔의 값을 객체의 프로퍼티에 바인딩합니다. SwiftUI에서 Binding은 모델과 뷰 간의 양방향 연결에 KeyPath를 사용합니다.
assign(to:on:) 연산자는 ReferenceWritableKeyPath와 객체를 받습니다. 퍼블리셔가 값을 방출하면 지정된 프로퍼티에 자동으로 기록됩니다. 이는 수동 할당 없이 상태를 관리하는 선언적 방법입니다.
import Combine
class SettingsViewModel: ObservableObject {
@Published var volume: Float = 0.5
var cancellables = Set<AnyCancellable>()
func bindSlider(publisher: AnyPublisher<Float, Never>) {
publisher
.assign(to: \SettingsViewModel.volume, on: self)
.store(in: &cancellables)
}
}
SwiftUI에서 @Binding은 부모 뷰와 자식 뷰를 연결하기 위해 KeyPath를 사용합니다. 부모는 Binding(get:set:) 또는 프로젝션($)을 통해 생성된 Binding
KeyPath를 사용하면 유연하고 타입 안전한 정렬 및 필터링 함수를 작성할 수 있습니다. 매번 클로저를 전달하는 대신 컬렉션을 정렬하려는 프로퍼티에 대한 KeyPath를 전달할 수 있습니다. 이렇게 하면 코드가 더 깔끔해지고 중복이 줄어듭니다.
함수 sorted
extension Sequence {
func sorted<Value: Comparable>(
by keyPath: KeyPath<Element, Value>,
ascending: Bool = true
) -> [Element] {
ascending
? self.sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
: self.sorted { $0[keyPath: keyPath] > $1[keyPath: keyPath] }
}
}
let sortedByName = people.sorted(by: \Person.name)
let sortedByAgeDesc = people.sorted(by: \Person.age, ascending: false)
이 구현은 클로저를 작성하지 않고도 모든 Comparable 프로퍼티로 정렬할 수 있습니다. 성별로 정렬하려면 KeyPath만 변경하면 됩니다: \Person.lastName. 사용자 지정 키(예: 이름 길이)로 정렬해야 하는 경우 내부적으로 map이 사용됩니다 — 개발자는 비표준 사례를 위해 자체 클로저를 작성할 수 있습니다.
AnyKeyPath는 KeyPath 계층 구조의 기본 타입으로, 특정 루트 및 값 타입에 대한 정보를 지웁니다. AnyKeyPath는 어떤 객체 타입에서 호출되었는지나 어떤 값 타입을 반환하는지 알지 못합니다. 이는 이기종 KeyPath를 컬렉션에 저장하거나 리플렉션에 유용합니다.
AnyKeyPath는 다른 타입의 다른 프로퍼티에 대한 경로 목록을 저장해야 할 때 사용됩니다. 예를 들어 UI 구성기에서 각 경로가 특정 프로퍼티를 구성하고 프로퍼티 타입이 다를 수 있습니다. AnyKeyPath를 사용하면 모든 경로를 통일된 방식으로 작업할 수 있습니다.
protocol Configurable {
func apply(_ keyPath: any PartialKeyPath<Self>, value: any)
}
extension Configurable {
func configure(_ pairs: (any KeyPath<Self, any>, any)...) {
for (path, value) in pairs {
apply(path, value: value)
}
}
}
extension Person: Configurable { }
// AnyKeyPath를 통한 프로퍼티 구성 허용
AnyKeyPath는 값을 검색할 때 타입 캐스팅이 필요합니다. 특정 타입이 지워졌기 때문입니다. 완전한 타입 안전성을 위해 타입화된 KeyPath
KeyPath는 KVC 문자열 키(value(forKey:))보다 훨씬 효율적입니다. 문자열 키는 Objective-C 런타임 메커니즘을 통해 해결되며, 문자열 파싱, 런타임 조회 및 타입 캐스팅이 포함됩니다. KeyPath는 네이티브 Swift 구조체를 기반으로 하는 정적 메커니즘입니다.
벤치마크에 따르면 KeyPath는 프로퍼티 접근에 대해 value(forKey:)보다 평균 10-20배 빠릅니다. 차이는 런타임 조회와 동적 디스패치가 없기 때문입니다. KeyPath는 충분한 컴파일러 최적화를 통해 메모리의 프로퍼티 오프셋에 직접 접근합니다.
func readWithKeyPath(person: Person) -> String {
person[keyPath: \Person.name]
}
func readWithKVC(person: Person) -> String {
person.value(forKey: "name") as! String
}
KVC는 느릴 뿐만 아니라 타입 안전하지 않습니다: “name” 문자열이 존재하지 않을 수 있고 as! String 캐스트가 충돌할 수 있습니다. KeyPath는 컴파일 타임에 프로퍼티의 존재와 타입 정확성을 보장합니다. 성능이 중요한 부분(애니메이션, 수천 개 항목이 있는 목록)에서는 KeyPath가 유일한 올바른 선택입니다.
자주 묻는 질문
KeyPath는 일반 값으로 전달할 수 있는 객체 프로퍼티에 대한 포인터입니다. person.name을 작성하는 대신 \Person.name 경로를 만들고 모든 Person 인스턴스에서 프로퍼티를 읽거나 쓰는 데 사용합니다.
KeyPath — 읽기 전용. WritableKeyPath — inout 컨텍스트를 통해 값 타입(struct)을 읽고 쓰기 위한 것. ReferenceWritableKeyPath — 추가 조건 없이 참조 타입(class)을 읽고 쓰기 위한 것.
KeyPath는 Combine(assign(to:on:)), SwiftUI(Binding, FocusState), 컬렉션 정렬(sorted(by:)), Core Data(NSSortDescriptor) 및 중첩 프로퍼티에 대한 안전한 접근에 널리 사용됩니다.
네, KeyPath는 완전히 타입 안전합니다. 경로 오류(오타, 잘못된 타입)는 런타임이 아닌 컴파일 타임에 발견됩니다. value(forKey:)는 문자열을 사용하며 키가 존재하지 않으면 NSUndefinedKeyException으로 충돌할 수 있습니다.
직접적으로는 불가능합니다 — Swift는 문자열에서 동적 KeyPath 생성을 지원하지 않습니다. 그러나 AnyKeyPath를 사용하여 정적으로 얻은 KeyPath를 저장하고 전달할 수 있습니다. 문자열을 통한 동적 프로퍼티 접근의 경우 Objective-C 런타임과 함께 value(forKey:)를 사용하세요.
요약
턴키 방식의 모바일 애플리케이션을 개발해 드립니다
IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.