Swift에서 Key-Path 표현식 쉽게 이해하기

woozoobro
7 min readNov 18, 2023

--

우리 파인더를 보면 요런 식으로 폴더에 대한 경로를 볼 수 있죠?

KeyPath는 지금과 유사하게 `어떤 특정한 타입`이 가진 프로퍼티에 접근할 때 `경로를 통해서 접근`을 하는 표현이에요.

SwiftUI에서 `ForEach`를 사용해 반복적인 뷰들을 그려줄 때

struct Item: Identifiable {
var id: UUID
var name: String
}

let items: [Item] = [ ... ]

ForEach(items, id: \.self) { item in
Text(item.name)
}

지금처럼 id: \.self를 작성하기도 하고, id: \.id 를 작성하기도 하죠?
위 처럼 작성할 때 생략된 부분은 id: \Item.self 혹은 id: \Item.id 입니다.

\.self를 사용했을 땐 각각의 항목들을 항목 자체로 식별하겠다는 거고
\.id의 경우 Item 모델이 가진 id라는 프로퍼티 경로 로 식별하겠다는 거에요!

이 프로퍼티의 경로라는게 잘 감이 안 잡히죠?

우리 어머니께서 냉장고의 냉장실의 위 선반에 있는 락앤락 반찬통에 있는 김치 꺼내 먹어라는 문자를 보내셨을 때 저희가 먹을 반찬 타입에 대한 키 패스를 어머니께서 알려주셨다고 할 수 있겠네요.

키패스(Key-Path) 표현식은 Swift 프로그래밍에서 중요한 요소 중 하나에요. 이 글에서는 키패스 표현식이 무엇이며 어떻게 사용되는지를 자세히 알아볼게요.

1. 키패스 표현식의 기본 구조

  • 키패스 표현식은 다음과 같은 형식을 가집니다:

\<#타입 이름#>.<#경로#>

  • 타입 이름: 이것은 구체적인 타입의 이름을 나타냅니다. 이 때, generic 파라미터를 포함할 수 있습니다. 예를 들어, `String`, `[Int]`, 또는 `Set<Int>`와 같은 형태가 될 수 있습니다.
  • 경로: 이 부분은 속성 이름, 서브스크립트, 옵셔널 체이닝 표현식, 강제 언래핑 표현식으로 구성됩니다. 각각의 키패스 컴포넌트는 원하는 만큼 순서대로 반복할 수 있으며, 이들을 조합하여 복잡한 경로를 지정할 수 있습니다.

2. 값에 접근하기

키패스 표현식을 사용하여 값을 접근하려면 해당 키패스를 배열이나 객체에 전달하면 됩니다.

struct SomeStructure {
var someValue: Int
}

let s = SomeStructure(someValue: 12)
let pathToProperty = \SomeStructure.someValue

let value = s[keyPath: pathToProperty]
// value는 12

3. 타입 이름 생략하기

  • 타입 추론이 가능한 경우, 타입 이름은 생략할 수 있습니다. 아래의 코드에서는 `\SomeClass.someProperty` 대신 `\.someProperty`를 사용하고 있습니다.
class SomeClass: NSObject {
@objc dynamic var someProperty: Int
init(someProperty: Int) {
self.someProperty = someProperty
}
}

let c = SomeClass(someProperty: 10)
c.observe(\.someProperty) { object, change in
// ...
}

4. Identity Key-Path: .self

키패스의 경로 부분에 self를 사용하여 identity key-path를 생성할 수 있습니다. 이는 전체 인스턴스를 가리키며, 변수 내의 데이터에 쉽게 접근하거나 변경할 수 있습니다.

var compoundValue = (a: 1, b: 2)
// (a: 10, b: 20)으로 대체 가능
compoundValue[keyPath: \.self] = (a: 10, b: 20)

5. 다중 프로퍼티 접근

키 패스 경로에는 여러 프로퍼티 이름을 포함할 수 있습니다. 이를 통해 중첩된 타입 내의 속성에 접근할 수 있어요.

struct OuterStructure {
var outer: SomeStructure
init(someValue: Int) {
self.outer = SomeStructure(someValue: someValue)
}
}

let nested = OuterStructure(someValue: 24)
let nestedKeyPath = \OuterStructure.outer.someValue

let nestedValue = nested[keyPath: nestedKeyPath]
// nestedValue는 24

6. Subscript 활용

키패스 경로 내에서는 서브스크립트를 활용할 수 있습니다.
다만, 서브스크립트의 파라미터 타입은 Hashable 프로토콜을 준수해야 합니다.

let greetings = ["hello", "hola", "bonjour", "안녕"]
let myGreeting = greetings[keyPath: \[String].[1]]
// myGreeting은 'hola'

7. 클로져와 함께 사용

키패스를 사용한 서브스크립트의 값은 변수나 리터럴로 지정할 수 있습니다. 값은 값(semantics)을 사용하여 키패스에서 캡처됩니다. 다음 코드에서는 키패스 표현식과 클로져 내에서 변수 `index`를 활용하여 값을 접근합니다.

var index = 2
let path = \[String].[index]
let fn: ([String]) -> String = { strings in strings[index] }

print(greetings[keyPath: path])
// "bonjour" 출력
print(fn(greetings))
// "bonjour" 출력

// 'index' 값을 변경해도 'path'에 영향을 미치지 않음
index += 1
print(greetings[keyPath: path])
// "bonjour" 출력

// 'fn'은 'index'를 캡처하므로 새로운 값 사용
print(fn(greetings))
// "안녕" 출력

8. 부작용

키패스 표현식의 부작용은 해당 표현식이 평가되는 시점에만 발생합니다. 따라서 함수 호출이 키패스 표현식 내에서 발생하더라도, 해당 함수는 표현식을 평가할 때 한 번만 호출되며, 키패스가 다시 사용될 때마다 다시 호출되지 않습니다.

func makeIndex() -> Int {
print("Made an index")
return 0
}

// 아래 줄은 makeIndex()를 호출합니다.
let taskKeyPath = \[Task][makeIndex()]
// "Made an index" 출력

// 'taskKeyPath'를 사용해도 makeIndex()를 다시 호출하지 않음
let someTask = toDoList[keyPath: taskKeyPath]

9. Objective-C와의 상호작용

Objective-C API와 상호작용하는 코드에서 키패스를 사용할 때 더 많은 정보가 필요한 경우, [Using Objective-C Runtime Features in Swift] 문서를 참고하세요. 또한, 키패스를 사용한 키-값 코딩과 키-값 관찰에 대한 자세한 내용은 [Key-Value Coding Programming Guide]와 [Key-Value Observing Programming Guide]를 참고하세요.

키패스 표현식은 Swift 프로그래밍의 강력한 기능 중 하나이며, 복잡한 데이터 모델을 다룰 때 효과적으로 사용할 수 있습니다. 이러한 기능을 활용하여 코드를 간결하고 읽기 쉽게 작성할 수 있습니다.

--

--