본문 바로가기
iOS/Swift

[Swift] KeyPath

by Bokoo14 2026. 3. 8.

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 기준 정렬