์ด์ „ ๊ธ€๋“ค์—์„œ Objective C์˜ KeyPath์™€ Swift์—์„œ์˜ KeyPath๋ฅผ ํ•œ๋ฒˆ์”ฉ ๋ณด์•˜๋‹ค. ๋ชจ์–‘์ด ์ข€ ๋‹ฌ๋ž์—ˆ๋Š”๋ฐ ์™œ ๋‹ค๋ฅธ์ง€์— ๋Œ€ํ•ด์„œ ์•Œ์•„๋ณด์ž. ์ด๋ฒˆ ๊ธ€์€ WWDC 17์„ ๊ธฐ์ค€์œผ๋กœ ํ•œ๋‹ค.

KeyPath in Swift 3

์œ„์˜ KVC, KVO in Swift ๊ธ€์—์„œ ๋ฐ”๋ผ๋ณธ KeyPath์ด์ „์— ์‚ฌ์šฉ๋˜๋˜ KeyPath๊ฐ€ ์žˆ์—ˆ๋‹ค.

// Swift 3 String Key Paths
 
@objcMembers class Kid : NSObject {
    dynamic var nickname: String = ""
    dynamic var age: Double = 0.0
    dynamic var bestFriend: Kid? = nil
    dynamic var friends: [Kid] = []
}
 
var ben = Kid(nickname: "Benji", age: 5.5)
let kidsNameKeyPath = #keyPath(Kid.nickname) // Swift 3
 
let name = ben.valueForKeyPath(kidsNameKeyPath)
ben.setValue("Ben", forKeyPath: kidsNameKeyPath)

#์„ ์‚ฌ์šฉํ•œ KeyPath๋ฅผ ํ†ตํ•ด Swift์—์„œ KVC๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ์‹์ด์—ˆ๋‹ค. Compile time์—๋Š” #keyPath(Kid.nickname)์„ String์œผ๋กœ ๋ณ€๊ฒฝํ•˜๊ณ  Objective C Runtime์„ ํ™œ์šฉํ•ด์„œ ๊ฐ์ฒด๋ฅผ ์ฐพ๊ฒŒ ๋œ๋‹ค. ์ฆ‰, ์ด๋ ‡๊ฒŒ ๊น”์Œˆํ•œ ๋ฐฉ์‹์œผ๋กœ KeyPath๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ๊ธฐ์กด์— String์„ ์‚ฌ์šฉํ–ˆ์„ ๋•Œ๋ณด๋‹ค ์•ˆ์ „์„ฑ์„ ๋†’ํ˜”์Œ์—๋„ ๋ถˆ๊ตฌํ•˜๊ณ , ๊ทผ๋ณธ์€ Objective C runtime์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๋‹ค๋Š” ๊ฒƒ์ด๋‹ค. Type์— ๋Œ€ํ•œ ์ •๋ณด๋„ ์—†๋Š” ์ˆœ์ˆ˜ํ•œ String์ด๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— Return value๋„ Any๋กœ ๋ฐ›์•˜์–ด์•ผ ํ–ˆ๋‹ค.

valueForKeyPath(_: String) -> Any
setValue(_, forKeyPath: String) -> Any

New KeyPath

Swift์—์„œ ์ œ๊ณตํ•˜๋Š” KeyPath์˜ ๋ชจ์–‘์€ ์œ„์™€ ๊ฐ™์ด ๋ณ€๊ฒฝ๋˜์—ˆ๋‹ค.

Swift์ด๊ธฐ ๋•Œ๋ฌธ์— Type ์ถ”๋ก ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค. ๋˜ํ•œ Sequence๋กœ ์—ฐ๊ฒฐํ•ด์„œ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ ์—ญ์‹œ ๊ฐ€๋Šฅํ•˜๋‹ค.

Optional๋„ ๊ฐ€๋Šฅํ•˜๊ณ , Subscript๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

Subscript๋กœ ์ง์ ‘ ์‚ฌ์šฉ๋„ ๊ฐ€๋Šฅํ•˜๋ฉฐ, Type ์ถ”๋ก ๊นŒ์ง€ํ•˜๋ฉด ์•„๋ž˜์™€ ๊ฐ™์ด ์‚ฌ์šฉ๋„ ํ•  ์ˆ˜ ์žˆ๋‹ค.

// Using Swift 4 KeyPaths
struct BirthdayParty { 
    let celebrant: Kid 
    var theme: String 
    var attending: [Kid]
}
 
let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) 
 
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] // == \.celebrant
 
bensParty[keyPath: \BirthdayParty.theme] = "Pirate" // == \.theme

๊ทธ๋ž˜์„œ ์ตœ์ข…์ ์œผ๋กœ ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ์ ์šฉํ•˜๋ฉด ์ด์™€ ๊ฐ™๊ฒŒ ๋œ๋‹ค. Objective-C runtime์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ  ๋ฐ”๋กœ ์ ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค. ๋˜ํ•œ Value type์ž„์—๋„ ์ฒ˜๋ฆฌ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค!

KeyPath Type

let birthdayKidsAgeKeyPath = \BirthdayParty.celebrant.age // KeyPath<BirthDayParty, Double>
let birthdayBoysAge = bensParty[keyPath: birthdayKidsAgeKeyPath] // Double

KeyPath๋ผ๋Š” Type์ด ์ƒ๊ฒผ๋‹ค. Base Type๊ณผ Property์˜ ์‹ค์ œ Type์„ ์ ์–ด์„œ ์„ ์–ธํ•  ์ˆ˜ ์žˆ๋‹ค.

func partyPersonsAge(party: BirthdayParty,
                     participantPath: KeyPath<BirthdayParty, Kid>) -> Double {
    let kidsAgeKeyPath = participantPath.appending(\.age)
    return party[keyPath: kidsAgeKeyPath] 
}

Type์œผ๋กœ ์žˆ๋Š” KeyPath๋ฅผ ๋ฐ›์•„ ์›ํ•˜๋Š” Type์„ ๋ฆฌํ„ดํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋งŒ๋“ค ์ˆ˜๋„ ์žˆ๊ฒ ๋‹ค.

The .appending Rule

์ด๋Ÿฐ ํŠน์ง•๋“ค์„ ํ†ตํ•ด KeyPath๋ผ๋ฆฌ appendํ•˜๋Š” ๊ฒƒ๋„ ๊ฐ€๋Šฅํ•˜๋‹ค.

๋ณด๋ฉด ์•Œ๊ฒ ์ง€๋งŒ, ์ˆœ์ฐจ์ ์œผ๋กœ Type์ด ๋งž๋Š” ๊ฒฝ์šฐ ์—ฐ์‚ฐ์„ ํ†ตํ•ด ์ตœ์ข…์ ์œผ๋กœ ์›ํ•˜๋Š” KeyPath๋ฅผ ์–ป์„ ์ˆ˜ ์žˆ๋‹ค.

Other KeyPaths

TypeDescription
AnyKeyPathํƒ€์ž…์ด ์ง€์›Œ์ง„ KeyPath
PartialKeyPath๋ถ€๋ถ„์ ์œผ๋กœ ํƒ€์ž…์ด ์ง€์›Œ์ง„ KeyPath
KeyPathRead-only
getํ•˜๋Š” ์šฉ๋„๋กœ๋งŒ ์‚ฌ์šฉ๊ฐ€๋Šฅ
WriteableKeyPathValue type instance์— ์‚ฌ์šฉ๊ฐ€๋Šฅ
โ€œ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œโ€ ๋ชจ๋“  Property์— ๋Œ€ํ•ด(var) read & write access ์ œ๊ณต
ReferenceWriteableKeyPathReference type instance์— ์‚ฌ์šฉ๊ฐ€๋Šฅ
โ€œ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œโ€ ๋ชจ๋“  Property์— ๋Œ€ํ•ด(var) read & write access ์ œ๊ณต

๋งŒ์•ฝ ๊ฐ™์€ KeyPath๋ฅผ ๊ณ„์†ํ•ด์„œ ์ ์–ด์ค˜์•ผ ํ•œ๋‹ค๋ฉด ์–ผ๋งˆ๋‚˜ ๋ถˆํŽธํ• ๊นŒ?

let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) 
 
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] // == \.celebrant
 
bensParty[keyPath: \BirthdayParty.theme] = "Pirate"
bensParty[keyPath: \BirthdayParty.theme] = "Villan"
bensParty[keyPath: \BirthdayParty.theme] = "Hero"

์ด๋Ÿฐ ๊ฒฝ์šฐ ์ด๋ฅผ Type์œผ๋กœ ์ •์˜ํ•œ ๋’ค ์žฌ์‚ฌ์šฉํ•˜๋ฉด ํŽธํ•  ๊ฒƒ ๊ฐ™๋‹ค. ์ด๋Ÿฐ ๊ฒฝ์šฐ writeableKeyPath๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค. Value Type์˜ Property์— ๋Œ€ํ•ด write๊นŒ์ง€ ๊ฐ€๋Šฅํ•ด์•ผํ•˜๊ธฐ ๋•Œ๋ฌธ์ด๋‹ค.

struct BirthdayParty { 
    let celebrant: Kid 
    var theme: String 
    var attending: [Kid]
}
 
let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) 
 
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] // == \.celebrant
 
let writeableKeyPath = \BirthdataParty.theme
 
bensParty[keyPath: writeableKeyPath] = "Pirate"
bensParty[keyPath: writeableKeyPath] = "Villan"
bensParty[keyPath: writeableKeyPath] = "Hero"

์ด๋Ÿฐ์‹์œผ๋กœ ๋‹ค์–‘ํ•œ KeyPath๊ฐ€ ์ค€๋น„๋˜์–ด ์žˆ๋‹ค. ์ง€๊ธˆ ๊ฐ™์€ ๊ฒฝ์šฐ๋Š” ์ˆ˜์ •ํ•˜๋Š” ์ž‘์—…(write)์„ ํ•˜๊ณ  ์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ž๋™์œผ๋กœ ํƒ€์ž… ์ถ”๋ก ํ•˜์—ฌ writeableKeyPath๊ฐ€ ๋œ ์ƒํƒœ์ด๋‹ค. ๊ทธ๋ž˜์„œ ๋ณ€์ˆ˜๋ช…๋„ ๊ทธ๋ ‡๊ฒŒ ์ง€์–ด์ฃผ์—ˆ๋‹ค.

class BirthdayParty { // Changed
    let celebrant: Kid 
    var theme: String 
    var attending: [Kid]
}
 
let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) 
 
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] // == \.celebrant
 
let referenceWriteableKeyPath = \BirthdataParty.theme
 
bensParty[keyPath: referenceWriteableKeyPath] = "Pirate"
bensParty[keyPath: referenceWriteableKeyPath] = "Villan"
bensParty[keyPath: referenceWriteableKeyPath] = "Hero"

์ด๋ฒˆ์—๋Š” Class๋กœ ๋ณ€๊ฒฝํ•ด์ฃผ์—ˆ๋‹ค. ์ด ๊ฒฝ์šฐ์—๋Š” type ์ถ”๋ก ํ•˜์—ฌ ReferenceWriteableKeyPath์œผ๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค.

class BirthdayParty { 
    let celebrant: Kid 
    let theme: String // Changed
    var attending: [Kid]
}
 
let bensParty = BirthdayParty(celebrant: ben, theme: "Construction", attending: []) 
 
let birthdayKid = bensParty[keyPath: \BirthdayParty.celebrant] // == \.celebrant
 
let keyPath = \BirthdataParty.theme
 
bensParty[keyPath: keyPath] = "Pirate" // Cannot Assign through subscript: 'keypath' is a read-only key path
bensParty[keyPath: keyPath] = "Villan" // Cannot Assign through subscript: 'keypath' is a read-only key path
bensParty[keyPath: keyPath] = "Hero"   // Cannot Assign through subscript: 'keypath' is a read-only key path

๋งŒ์•ฝ ๋ณ€๊ฒฝํ•˜๊ณ ์ž ํ•˜๋Š” property์˜ ์„ ์–ธ์ด let์œผ๋กœ ๋ณ€๊ฒฝ๋˜๋ฉด ์–ด๋–จ๊นŒ? ์ด ๊ฒฝ์šฐ์—๋Š” type ์ถ”๋ก ์ด ๋™์ž‘ํ•˜์—ฌ KeyPath๋กœ ๋ณ€๊ฒฝ๋œ๋‹ค. ์ด ๊ฒฝ์šฐ, write ๋™์ž‘์ด ๋ถˆ๊ฐ€ํ•˜๋‹ค.

Key Path ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉํ•˜๊ธฐ

Swift5.2 ์—์„œ๋Š” Key Path Expressions ์„ ํ•จ์ˆ˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” Proposal์ด ๊ตฌํ˜„๋˜์—ˆ๋‹ค. \Root.value๋ฅผ ์ ์Œ์œผ๋กœ์จ (Root) -> Value์˜ ์™€ ๊ฐ™์€ ํ•จ์ˆ˜ ํ˜•ํƒœ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋‹ค. ์—ฌ๊ธฐ์„œ rootType์€ keypath๊ฐ€ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ํƒ€์ž… ์ด๋ฆ„์ด๋‹ค.

ํ•ด๋‹น ์ด์Šˆ ๋ฐœ์ œ์ž๋Š”, users.map { $0.email }๊ณผ ๊ฐ™์€ ๊ตฌ๋ฌธ์ด ์žˆ์„ ๋•Œ, ๋ณด๋‹ค ๊น”๋”ํ•˜๊ฒŒ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค ํ–ˆ๊ณ , ๊ทธ ๋‹ต์ด keyPath๋ผ ํ–ˆ๋‹ค. ์ตœ์ข…์ ์œผ๋กœ users.map(\.email)๊ณผ ๊ฐ™์ด ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์„ ์ œ์•ˆํ–ˆ๊ณ , ์ด๋ฅผ keypath๋ฅผ ์ž…๋ ฅํ–ˆ์„ ์‹œ ์ด๋ฅผ clouser ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ฒƒ์œผ๋กœ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ž๊ณ  ํ–ˆ๋‹ค.

// You write this:
let f: (User) -> String = \User.email
 
// The compiler generates something like this:
let f: (User) -> String = 
{ 
    kp in { 
        root in root[keyPath: kp] 
    } 
}(\User.email)

\User.email์ด๋ผ๊ณ  ์ ์„ ์‹œ, Compiler๊ฐ€ ์ด๋ฅผ ์•„๋ž˜์™€ ๊ฐ™์€ function์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ , ์ด๋ฅผ ํ†ตํ•ด ์œ„์— ์ ์€ ๋™์ž‘์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•˜์ž ์ œ์•ˆํ–ˆ๋‹ค. ์‹ค์ œ๋กœ ๊ตฌํ˜„๋˜์–ด ์‚ฌ์šฉ์ด ๊ฐ€๋Šฅํ•˜๋‹ค.

๋งˆ๋ฌด๋ฆฌ

์ด๋ ‡๊ฒŒ ์ €๋ฒˆ ๊ธ€์—์„œ ์ œ๋Œ€๋กœ ์•Œ์•„๋ณด์ง€ ๋ชปํ–ˆ๋˜ KeyPath์— ๋Œ€ํ•ด์„œ ์ •๋ฆฌํ•ด๋ณด์•˜๋‹ค. ๋!

Reference