Swift를 쓰다 보면 \.name, \User.age 같은 문법을 마주치게 된다.
Combine의 assign(to:on:), SwiftUI의 Binding, sorted(by:) 같은 곳에서 자주 등장한다.
이 글에서는 KeyPath가 무엇인지, 왜 필요한지, 실제로 어디서 어떻게 쓰이는지를 정리한다.
KeyPath란?
KeyPath는 "어떤 타입의 어떤 프로퍼티로 가는 경로"를 값으로 표현한 것이다.
일반적인 프로퍼티 접근은 특정 인스턴스에 묶여 있다.
struct User {
var name: String
var age: Int
}
let user = User(name: "보경", age: 25)
print(user.name) // "보경" — user라는 특정 인스턴스의 name에 접근
반면 KeyPath는 인스턴스 없이 경로 자체를 값으로 들고 다닐 수 있다.
let namePath = \User.name // KeyPath<User, String> — 경로를 변수에 담음
// 나중에 인스턴스에 적용
print(user[keyPath: namePath]) // "보경"
"지금 당장 어떤 인스턴스의 값을 꺼내는 것"이 아니라, "나중에 어떤 인스턴스에든 적용할 수 있는 경로" 를 만드는 것이다.
기본 문법
KeyPath는 백슬래시(\)로 시작한다.
\타입.프로퍼티
struct User {
var name: String
var age: Int
}
let namePath = \User.name // KeyPath<User, String>
let agePath = \User.age // KeyPath<User, Int>
let user = User(name: "보경", age: 25)
// [keyPath:] 서브스크립트로 적용
print(user[keyPath: namePath]) // "보경"
print(user[keyPath: agePath]) // 25
타입 추론이 가능한 컨텍스트에서는 타입명을 생략하고 \.name처럼 쓸 수 있다.
// 타입을 이미 알고 있는 컨텍스트에서는 생략 가능
users.map(\.name)
users.sorted(by: \.age)
KeyPath의 종류
KeyPath는 프로퍼티의 특성에 따라 세 가지 타입으로 나뉜다. 컴파일러가 자동으로 결정해준다.
struct User {
let id: Int // let — 읽기 전용
var name: String // var — 읽기/쓰기
}
class Profile {
var bio: String = "" // class — 참조 타입
}
| 종류 | 언제 | 읽기 | 쓰기 |
| KeyPath | let 프로퍼티 | ✅ | ❌ |
| WritableKeyPath | var 프로퍼티 (값 타입) | ✅ | ✅ |
| ReferenceWritableKeyPath | var 프로퍼티 (참조 타입) | ✅ | ✅ |
// WritableKeyPath — 값을 쓸 수도 있다
var user = User(id: 1, name: "보경")
user[keyPath: \.name] = "철수"
print(user.name) // "철수"
// KeyPath — 읽기만 가능
// user[keyPath: \.id] = 2 // ❌ 컴파일 에러
중첩 경로
프로퍼티 안의 프로퍼티로 가는 경로도 표현할 수 있다.
struct Address {
var city: String
}
struct User {
var name: String
var address: Address
}
let cityPath = \User.address.city // KeyPath<User, String>
let user = User(name: "보경", address: Address(city: "서울"))
print(user[keyPath: cityPath]) // "서울"
경로를 .으로 이어 붙이면 된다. 중첩 깊이에 제한은 없다.
어디서 쓰이나
1. map, sorted — 클로저 대신 KeyPath로 간결하게
let users = [
User(name: "보경", age: 25),
User(name: "철수", age: 30),
User(name: "영희", age: 20),
]
// 클로저 방식
let names = users.map { $0.name }
let sorted = users.sorted { $0.age < $1.age }
// KeyPath 방식 — 더 간결하다
let names = users.map(\.name) // ["보경", "철수", "영희"]
let sorted = users.sorted(by: \.age) // age 기준 정렬
Swift 표준 라이브러리에서 KeyPath를 받는 오버로드를 제공하기 때문에 가능하다.
2. Combine ㅡ assign(to:on:)
viewModel.$isValid
.assign(to: \.isEnabled, on: button)
// ↑
// KeyPath<UIButton, Bool>
// "button의 isEnabled 프로퍼티로 가는 경로"
Publisher의 출력값을 특정 객체의 프로퍼티에 자동으로 할당할 때 KeyPath를 사용한다.
button.isEnabled = value를 직접 쓰는 대신, 경로를 넘겨서 Combine이 처리하게 한다.
3. SwiftUI — Binding
SwiftUI 내부에서 Binding을 만들 때도 KeyPath를 활용한다.
// 내부적으로 이런 방식으로 동작한다
Binding(
get: { user[keyPath: \.name] },
set: { user[keyPath: \.name] = $0 }
)
4. 제네릭 함수 — 어떤 타입, 어떤 프로퍼티든 유연하게
func extract<T, V>(_ items: [T], keyPath: KeyPath<T, V>) -> [V] {
items.map { $0[keyPath: keyPath] }
}
extract(users, keyPath: \.name) // ["보경", "철수", "영희"]
extract(users, keyPath: \.age) // [25, 30, 20]
타입을 하나로 고정하지 않고, 어떤 타입의 어떤 프로퍼티든 받아서 처리할 수 있다.
KeyPath 없이 같은 동작을 구현하려면 클로저를 매번 작성해야 한다.
// KeyPath 없이 — 매번 클로저를 써야 한다
extract(users) { $0.name }
extract(users) { $0.age }
일반 프로퍼티 접근과의 차이
| 일반 프로퍼티 | 접근 | KeyPath |
| 형태 | user.name | \User.name |
| 인스턴스 필요 여부 | 필요 | 불필요 (나중에 적용) |
| 값으로 전달 가능 | ❌ | ✅ |
| 제네릭과 조합 | 어려움 | 자연스러움 |
정리
KeyPath는 "프로퍼티로 가는 경로 자체"를 값으로 표현한 타입이다.
특정 인스턴스에 묶이지 않기 때문에, 나중에 어떤 인스턴스에든 [keyPath:]로 적용할 수 있다. 덕분에 map, sorted 같은 고차 함수에서 클로저를 대체하거나, Combine의 assign, SwiftUI의 Binding 같은 곳에서 선언적이고 타입 안전한 코드를 작성할 수 있다.
// 경로를 값으로 — 인스턴스 없이 경로만 표현
let path = \User.name // KeyPath<User, String>
// 나중에 인스턴스에 적용
user[keyPath: path] // "보경"
// 고차 함수에 바로 전달
users.map(\.name) // ["보경", "철수", "영희"]
users.sorted(by: \.age) // age 기준 정렬
'iOS > Swift' 카테고리의 다른 글
| [Swift] IUO (Implicitly Unwrapped Optional, 옵셔널 묵시적 추출) (0) | 2026.03.15 |
|---|---|
| [Swift] borrowing, consuming (1) | 2026.03.15 |
| [SwiftUI] @State, @Binding, Projected Value (0) | 2026.03.08 |
| [Combine] throttle와 debounce (0) | 2025.05.20 |
| [iOS] ViewLife Cycle (UIKit vs SwiftUI) (0) | 2024.05.20 |