Swift

[Swift 5.6] Swift 5.6 추가된 기능들

Phililip
728x90

안녕하세요.

 

이번에는 Swift 5.6에 새롭게 추가된 기능들에 대해 알아볼게요.

 

Swift 5.6은 현재 베타 버전으로 올라온 Xcode 13.3에서 사용 가능하니 참고 부탁드려요.

 

 


 

# 1. any 키워드 

SE-0335에 소개된 신규 키워드 any는 existential types 앞에 붙는(앞으로 꼭 붙어야 할) 키워드예요.

 

 

좀 더 구체적인 설명에 앞서 다른 얘기를 먼저 해볼게요.

 

프로토콜을 사용하면 그 프로토콜을 채택한 구조체 또는 클래스에서 프로토콜의 요구사항을 준수하도록 구현해줘야 합니다.

 

 

예를 들어서,

 

아래처럼 Vehicle이란 프로토콜이 있고,

 

protocol Vehicle {
    func travel(to destination: String)
}

 

 

Car라는 구조체가 Vehicle 프로토콜을 채택했으면, 요구사항인 travel(to:) method를 구현해줘야 합니다.

 

protocol Vehicle {
    func travel(to destination: String)
}

struct Car: Vehicle {
    func travel(to destination: String) {
        print("I'm driving to \(destination)")
    }
}

 

 

그리고 저희는 이렇게 쓰겠죠?

 

let vehicle = Car()
vehicle.travel(to: "London")    // I'm driving to London

 

 

함수 안에서 제네릭 타입으로 받아서 사용하는 것도 가능합니다.

 

func travel<T: Vehicle>(to destinations: [String], using vehicle: T) {
    for destination in destinations {
        vehicle.travel(to: destination)
    }
}

travel(to: ["London", "Amarillo"], using: vehicle)
// I'm driving to London
// I'm driving to Amarillo

 

위 코드는 vehicle이라는 변수가 Car 타입이기 때문에,

 

함수 안에서 Car 인스턴스의 travel(to:) method를 호출한다는 것이 컴파일 타임 때 결정됩니다. (static dispatch 방식이라고 하죠)

 

 

 

비슷한 코드지만 살짝 다른 아래 코드를 한 번 볼까요?

 

let vehicle2: Vehicle = Car()   🤔
vehicle2.travel(to: "Glasgow")

 

vehicle2에는 Car 인스턴스를 넣지만, Vehicle(위에서 선언한 프로토콜) 타입으로 선언을 했습니다.

 

이렇게 캡슐화를 한 것처럼 프로토콜 타입으로 선언한 것을 existential type이라고 불러요.

(프로토콜을 준수했다면 어떤 값, 어떤 타입이든 상관없다는 거죠.)

 

 

물론 existential type이어도 아래처럼 사용이 가능해요.

 

func travel2(to destinations: [String], using vehicle: Vehicle) {
    for destination in destinations {
        vehicle.travel(to: destination)
    }
}

travel2(to: ["London", "Amarillo"], using: vehicle2)
// I'm driving to London
// I'm driving to Amarillo

 

하지만 한 가지 큰 차이점이 있습니다.

 

vehicle2는 Vehicle 프로토콜을 준수하는 것이면 모든 가능하기 때문에, travle(to:) method가 구체적으로 어떤 함수가 불릴지 컴파일 타임 때 알 수 없어요.

 

그렇기 때문에 런타임 때 어떤 함수가 불릴지 결정이 되고, 그로 인해 최적화를 할 수 없어요. (dynamic dispatch 방식이라고 합니다.)

 

 

 

그런데 static dispatch 방식과 dynamic dispatch 방식의 코드가 상당히 유사해서,

 

"이게 existential type으로 선언된 건가?" 라는 게 한눈에 안 들어오지 않습니다.

 

 

그래서!!

 

existential type으로 선언된 곳에 any라는 키워드로 마킹해두는 것이죠.

 

let vehicle3: any Vehicle = Car()    ✅
vehicle3.travel(to: "Glasgow")

func travel3(to destinations: [String], using vehicle: any Vehicle) {
    for destination in destinations {
        vehicle.travel(to: destination)
    }
}

 

이렇게요ㅎㅎㅎ

 

 

Swift 5에서는 any 키워드를 소개한 정도에 그치지만

 

❗️❗️❗️ 앞으로 나올 Swift 6에서는 existential type에 any를 꼭 붙여주셔야 할 것 같아요.❗️❗️❗️

 

any를 붙이지 않으면 에러가 발생할 거라고 합니다. 참고해주세요!

 

 

 

 

# 2. Type placeholders

SE-0315에 소개된 내용이에요.

 

타입을 직접 명시하는 대신 _(언더바)를 사용해서 타입을 유추시킬 수 있어요. (Type placeholder라고 부른답니다.)

 

let score1 = 5
let score2: Int = 5
let score3: _ = 5

 

 

 

그럼 type placeholder라는 건 언제 쓰면 좋냐??

 

컴파일러가 타입 추론을 저희가 원하는 방향으로 하지 않을 경우, 유용하게 사용 가능합니다.

 

 

 

예를 들어,

 

학생별로 시험 성적을 가지고 있는 Dictionary가 있다고 해볼게요.

 

var results1 = [
    "Cynthia": [],
    "Jenny": [],
    "Trixie": [],
]

print(type(of: results1))    // Dictionary<String, Array<Any>>

 

key에 대한 타입은 String으로 잘 유추했지만,

 

시험 성적에 해당하는 value는 Array <Int>가 아닌 Array<Any>로 유추를 했죠?

(빈 array다 보니 컴파일러 입장에선 배열 요소에 Int가 들어올지 String이 들어올지 모르기 때문에 Any로 정한 것이에요.)

 

 

그래서 이런 경우엔 기존 방식대로라면 아래처럼 key, value 타입을 모두 적어줬겠지만,

 

var results2: [String: [Int]] = [
    "Cynthia": [],
    "Jenny": [],
    "Trixie": [],
]

print(type(of: results2))    // Dictionary<String, Array<Int>>

 

 

 

아래처럼 type placeholder를 활용해서, 컴파일러가 모를만한 타입(예시에선 value 겠죠?)은 직접 선언해주고, key는 유추해라!! 라고 하는거죠.

 

var results3: [_: [Int]] = [
    "Cynthia": [],
    "Jenny": [],
    "Trixie": [],
]

print(type(of: results3))    // Dictionary<String, Array<Int>>

 

 

Optional도 가능해요!!

 

var results4: [_: _?] = [
    "Cynthia": [],
    "Jenny": [],
    "Trixie": [],
    "Philip": nil
]

print(type(of: results4))    // Dictionary<String, Optional<Array<Any>>>

 

 

 

# 3. CodingKeyRepresentable 프로토콜

SE-0320에 소개된 내용이에요.

 

Dictionary를 JSON String으로 바꾸려면, 아래처럼 JSONEncoder를 사용했습니다.

 

let dict = ["Philip": 19, "Park": 29]
let dictData = try JSONEncoder().encode(dict)
let dictJson = String(decoding: dictData, as: UTF8.self)    // "{"Philip":19,"Park":29}"

 

 

근데 문제가 있어요.

 

Dictionary key에 String 또는 Int 타입이 아닌 Double, enum 같은 타입이 들어가는 경우라면??

 

enum OldSettings: String, Codable {
    case name
    case twitter
}

let oldDict: [OldSettings: String] = [.name: "Philip", .twitter: "known..."]
let oldData = try JSONEncoder().encode(oldDict)
let oldJson = String(decoding: oldData, as: UTF8.self)    // "["twitter","known...","name","Philip"]"

 

JSON 형태처럼 중괄호로 감싸진게 아니라, 배열처럼 대괄호로 감싸졌고 모두 콤마로 분리가 되어 있어요.

 

 

이번에 추가된 CodingKeyRepresentable 프로토콜을 사용하면, 정상적인 JSON String으로 변환이 가능합니다.

 

enum NewSettings: String, Codable, CodingKeyRepresentable {
    case name
    case twitter
}

let newDict: [NewSettings: String] = [.name: "Philip", .twitter: "known..."]
let newData = try! JSONEncoder().encode(newDict)
let newJson = String(decoding: newData, as: UTF8.self)    // "{"twitter":"known...","name":"Philip"}"

 

👍 👍

 

 

 

커스텀 구조체를 key로 사용하고 싶다면, CodingKeyRepresentable 프로토콜을 아래처럼 준수시킨 뒤 사용하세요.

 

struct MyCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?
    
    init(stringValue: String) {
        self.stringValue = stringValue
        self.intValue = Int(stringValue)
    }
    
    init(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
}

struct MyType: Codable, Hashable, CodingKeyRepresentable {
    let name: String
    
    var codingKey: CodingKey {
        return MyCodingKey(stringValue: name)
    }
    
    init(name: String) {
        self.name = name
    }

    init?<T>(codingKey: T) where T : CodingKey {
        name = codingKey.stringValue
    }
}

let customKeyDict = [MyType(name: "Philip"): 19, MyType(name: "Park"): 29]
let customKeyData = try! JSONEncoder().encode(customKeyDict)
let customKeyJson = String(decoding: customKeyData, as: UTF8.self)    // "{"Philip":19,"Park":29}"

 

 

 

# 4. #unavailable

SE-0290에 소개된 내용이에요.

 

특정 iOS 버전 이상인 경우 별도의 처리가 필요할 때 #available을 사용했죠??

 

if #available(iOS 15, *) {
    // iOS 15 이상은 여기
} else {
    // iOS 14 이하는 여기
}

 

 

Swift 5.6에는 특정 버전을 제외하고 별도 처리를 넣고 싶을 경우를 위해 #unavailable을 제공합니다.

 

if #unavailable(iOS 15) {
    // iOS 15 말고 나머지 버전은 여기
}

 

 

다만, 아직은 wildcard(*)는 제공해주고 있지 않아요..ㅠㅠ

(언젠간 제공해줄 것 같은 느낌이....)

 

if #unavailable(iOS 15, *) {    // Platform wildcard '*' is always implicit in #unavailable
    // ...
}

 

 

 

 

## 참고 

- https://www.hackingwithswift.com/articles/247/whats-new-in-swift-5-6

- https://github.com/apple/swift-evolution/blob/main/proposals/0335-existential-any.md

- https://github.com/apple/swift-evolution/blob/main/proposals/0315-placeholder-types.md

- https://github.com/apple/swift-evolution/blob/main/proposals/0320-codingkeyrepresentable.md

- https://github.com/apple/swift-evolution/blob/main/proposals/0290-negative-availability.md

 

 


 

이번 글은 여기서 마무리.

 

 

 

반응형