반응형

Hacking with Swift 사이트의 강좌 번역본입니다.

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

What’s new in Swift 5.6

타입 자리표시자(Type placeholders), 사용할수 없는(unavailable) 검사, Codable 개선, 등등.

Swift 5.6은 Swift 6.0에 가까워짐에 따라 다른 기능을 개선함으로써, 언어에 또 다른 새로운 기능을 도입했습니다. 이 글에서 주요 변경사항을 소개하고자 고, 변경사항을 직접 확인할 수 있도록 몇가지 예제를 제공합니다.

 : 코드 샘플을 사용해보고 싶으면 Xcode playground를 다운로드 할 수 있습니다.

실존적인 any 도입(Introduce existential any)

SE-0335는 실존적인 타입(existential types)을 표시하는 새로운 any 키워드를 도입했고, 비록 난해(esoteric)하게 들릴주 있지반 건너뛰지 말아주세요: 이는 큰 변화이고, 이후 버젼의 Swift 코드에 많은 영향(break)을 끼칠수 있습니다.

프로토콜은 메소드를 반드시 구현해야 하는 것처럼, 반드시 지켜야 하는 준수하는 타입을 요구하는 것을 지정할 수 있습니다. 따라서 종종 다음과 같은 코드를 작성합니다.

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")

프로토콜을 함수에서 제네릭(generic) 타입 제약사항(constraints)처럼 사용하는 것이 가능하며, 특정 프로토콜을 준수하는 모든(any) 종류의 데이터로 동작할 수 있는 코드를 작성한다는 것을 의미합니다. 예를들어, 다음은 Vehicle을 준수하는 모든(any) 종류의 타입에서 동작합니다.

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

travel(to: ["London", "Amarillo"], using: vehicle)

해당 코드를 컴파일 할때, Swift는 Car 인스턴스로 된 travel()을 호출하는 것을 볼 수 있고, travel() 함수를 직접 호출하기 위한 최적화된 코드를 만들수 있습니다. - static dispatch*로 알려진 프로세스입니다.

프로토콜을 사용하는 두번째 방법이고 지금까지 사용했던 다른 코드와 매우 유사하기 때문에 모두 중요합니다..

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

여기에서 여전히 Car 구조체를 만들지만, Vehicle 처럼 저장합니다. 이는 단순히 기본 정보를 숨기는 단순한 문제가 아니라, 이러한 Vehicle 타입은 실존적인 타입(existential type)이라는 전혀 다른 타입입니다: 새로운 데이터 타입은 Vechicle 프로토콜을 준수하는 모든 타입의 모든 값을 저장하는 것이 가능합니다.

중요 : 실존적인 타입(existential type)은 some 키워드를 사용하는 불분명한 타입(opaque types)과는 다릅니다. 예를 들어, some View는 항상 지정한 제약조건을 모두 준수하는 특정 타입을 표현해야 합니다.

다음과 같이 함수에서 실존적인 타입(existential type)을 사용할 수 있습니다.

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

다른 travel() 함수와 비슷해 보이지만, Vechicle 객체의 모든 종류를 허용하기에, Swift는 더 이상 동일한 최적화를 할 수 없습니다. - 동급의 static dispatch보다 성능이 낮은 dynamic dispatch*라고 하는 프로세스를 사용해야 합니다. 따라서, Swift는 프로토콜의 2가지 사용법이 매우 비슷해 보이고, 실제로 더 느리고, 실존적인 버젼(existential version)의 함수를 작성하는 것이 더 쉬웠습니다.

이 문제를 해결하기 위해서, Swift 5.6 은 실존적인 타입(existential types)을 사용하는 새로운 any 키워드를 도입으며, 코드에서 실존(existentials) 효과를 정확히 알수 있도록 했습니다. Swift 5.6에서 이러한 새로운 동작이 활성화 되고 동작되지만, 이후의 Swift 버젼에서는 이를 사용하지 않으면 경고를 하고, Swift 6에서는 오류도 발생할 계획입니다 - any 를 사용해서 실존적인 타입(existential types)을 표시해야 합니다.

다음과 같이 작성합니다.

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)
    }
}

이러한 결론에 도달하기까지 많은 설명이 필요하지만, 의미가 있길 바랍니다 : Vechicle을 준수하거나 제네릭 제약조건으로 사용할때, Vehicle로 작성할것이지만, 이를 사용할때 자체적인 타입 처럼 any Vehicle를 사용해야 합니다.

이는 Swift에서 큰 변화입니다. 다행히, Swift 팀은 천천히 진행하고 있습니다. - 수락 결정(acceptance decision)에서 다음과 같이 말했습니다.

“목표는 현재 배포한 Swift와 최소한 바로 이전에 배포에서는 경고없이 컴파일되는 코드를 작성하는 것이고, 이후에 사용자에게 기존 언어 모드에서 새로운 구문을 알리기 위해 경고를 도입하는 것입니다. 마지막으로 새로운 주요 언어 버젼에서만, 예전 구문을 제거하거나 용도를 변경할 수 있습니다.

타입 자리표시자(Type placeholders)

SE-0315는 값의 일부만 명시적으로 지저아고 나머지는 타입 추론으로 채울수 있는 타입 자리표시자(type placeholders)의 개념을 도입했습니다.

실제로, Swift가 타입 추론을 사용하길 원하는 모든 곳에서 _를 작성하는 것을 의미하고, 다음 코드 3줄이 같다는 것을 의미합니다.

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

이러한 간단한 예제에서 타입 자리표시자를 아무것도 추가하지 않았지만, 컴파일러가 타입의 일부가 아닌 전체를 추론할 수 있을때 유용합니다. 예를들어, 학생의 이름과 올해의 모든 시험 결과에 대한 딕셔너리(dictionary)를 만들때, 다음과 같이 작성할 수 있습니다.

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

Swift는 문자열을 키로 사용하고, Any 배열을 값으로 사용하는 것을 추론할 것입니다. - 대부분 여러분이 원하는 것이 아닙니다. 다음과 같이 전체 타입을 명시적으로 지정할 수 있습니다.

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

하지만, 타입 자리표시자(type placeholders)를 사용하면 컴파일러가 추론할 부분에 _를 사용할 수 있습니다 - 이는 원하는 정확한 타입과 장소와 함께 이 부분은 타입 추론을 사용해야 한다라고 명시적으로 말하는 방법입니다.

따라서 다음과 같이 작성할 수 있습니다.

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

보다시피, _은 명시적으로 타입 추론을 요청하지만, 여전히 정확한 배열 타입을 지정할 기회를 가지고 있습니다.

** 팁 ** : 타입 자리표시자는 옵셔널 타입일수도 있습니다. - Swift가 옵셔널 타입을 추론하도록 하기 위해서 _?를 사용합니다.

타입 자리표시자는 함수 시그니쳐(signatures)를 작성하는 데 영향을 주지는 않습니다. : 여전히 매개변수와 반환 타입 모두 제공해야 합니다. 하지만, 프로토타입을 시험하느라 바쁠때 목적에 부합하다는 것을 발견했습니다 : 컴파일러에게 어떤 타입을 추론하도록 지시하는 경우에 Xcode가 코드를 완성하기 위해 Fix-it 하라는 메시지를 자주 보여줍니다.

예를들어, 다음과 같이 플레이어를 생성하는 코드를 작성할 수 있습니다.

struct Player<T: Numeric> {
    var name: String
    var score: T
}

func createPlayer() -> _ {
    Player(name: "Anonymous", score: 0)
}

createPlayer()에 대한 반환 타입을 지정하지 못하면, 컴파일러는 오류가 발생할 것입니다. 하지만, Swift에게 타입을 추론하도록 요청하는 것처럼, Xcode에서의 오류는 _ Player으로 교체하는 Fix-it을 제공합니다. - 좀 더 복잡한 타입을 다룰때 상당한 수고(hassle)를 덜 수 있다는 것을 상상할 수 있습니다.

긴 타입의 주석(annotations)을 단순화 하는 방법으로 타입 자리표시자(type placeholders)를 생각해 보세요 : 관련이 적은부분이나 상용구 부분을 모두 밑줄로 교체할수 있고, 중요한 부분은 철자로 남겨둬서 코드를 더 읽기 쉽게 만들 수 있습니다.

KeyedContainer에서 Dictionary 키로 String/Int이 아닌것을 허용

SE-0320는 키가 없는 컨테이너(containers) 보다는 키가 있는 컨테이너로 인코딩하기 위해서 딕셔너리에서 String이나 Int가 아닌 키를 허용하는 새로운 CodingKeyRepresentable 프로토콜을 도입했습니다.

왜 중요한지 이해하기 위해서는, 우선 CodingKeyRepresentable가 제자리에 없는 동작을 볼 필요가 있습니다. 예를 들어, 이전 코드는 딕셔너리에 키로 열거형 케이스를 사용하고나서 JSON으로 인코딩하고 결과 문자열을 출력합니다.

import Foundation

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

let oldDict: [OldSettings: String] = [.name: "Paul", .twitter: "@twostraws"]
let oldData = try JSONEncoder().encode(oldDict)
print(String(decoding: oldData, as: UTF8.self))

열거형이 String 원시(raw) 값이 있지만, 딕셔너리 키는 String이나 Int가 아니며 출력 문자열은 [twitter,@twostraws, name, Paul]이 될 것입니다.-키/값 쌍이 아닌 4개의 분리된 문자열 값. Swift는 디코딩에서 인식할만큼 충분히 똑똑하고, 각 쌍 내부에서 교차하는 문자열을 원래 열거형 키와 문자열 값으로 일치시키지만, 해당 JSON을 서버에 전송하는 경우에는 별 도움이 되지 않습니다.

새로운 CodingKeyRepresentable은 이를 해결하며, 새로운 딕셔너리 키를 정확히 작성하게 해줍니다. 하지만, Codable JSON 작성하는 방법이 변경되므로, 새로운 동작을 위해서는 다음과 같이 CodingKeyRepresentable을 준수하도록 명시적으로 추가해줘야 합니다.

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

let newDict: [NewSettings: String] = [.name: "Paul", .twitter: "@twostraws"]
let newData = try! JSONEncoder().encode(newDict)
print(String(decoding: newData, as: UTF8.self))

Swift 외부에서 매우 유용한, {twitter:@twostraws,name:"Paul”}가 출력될 것입니다.

사용자정의 구조체를 키(keys)로 사용하는 경우에, CodingKeyRepresentable을 준수하도록 할수 있고 데이터를 문자열로 변환하는 자체 메소드를 제공할 수도 있습니다.

사용할수 없는 조건(Unavailability condition)

SE-0290 #available 형식의 반대로 사용가능한지 검사가 실패하면 실행하는 #unavaliable을 도입했습니다.

이는 이전에 특정 운영체제를 사용할 수 없는 경우에만 코드를 실행하기 위해 비어 있는 참(true) 블럭으로 #available을 사용하는 곳에서 특히 유용할 것입니다. 그래서 다음과 같이 작성하기 보다는

if #available(iOS 15, *) { } else {
    // Code to make iOS 14 and earlier work correctly
}

이제는 다음과 같이 작성할 수 있습니다.

if #unavailable(iOS 15) {
    // Code to make iOS 14 and earlier work correctly
}

이 문제는 단지 이론적인 것만이 아니었습니다. - 비어 있는 #availabe 블럭을 iOS 13 기반에서의 Scene 기반의 UIKit API로 전환할때 매우 일반적인 해결방법 이었습니다.

반대(flipped)되는 동작과는 별개로 #available #unavailable간의 한가지 차이점은 플랫폼 와일드카드 ‘*’ 입니다. 잠재적으로 미래의 플랫폼을 허용하기 위해서 #available을 사용하며, if #available(ios 15, *)을 사용하면 iOS 15 버젼 15 이후와 모든 다른 플랫폼에서 일부 코드를 사용가능한 것을 표시합니다 - macOS, tvOS, watchOS, 미래의 알수 없는 플랫폼.

#unavailable을 사용하면 이 동작은 의미가 없습니다 : if #unavailable(iOS 15, *)을 사용하면, iOS 14 이전에서 실행해야 합니다 또는 macOS, tvOS, watchOS, Linux, 등도 포함해야 하고, iOS 15는 사용할 수 없는건가요? 이러한 모호한 상황을 피하기 위해, 플랫폼 와일드 카드는 #unavailable 에서 허용되지 않습니다 : 구체적인 목록의 플랫폼만 테스트 대상으로 간주됩니다.

더 많은 동시성 변경사항(More concurrency changes)

Swift 5.5에서 동시성 관련해서 많은 기능이 추가되었고, 5.6은 이러한 기능들을 더 안전하고 일관성있게 개선하는 동시에 Swift 6에서의 변경사항을 위해 더 노력하고 있습니다.

가장 큰 변화는 코드의 완전하고 엄격한 동시성 검사를 위한 로드맵을 제시하는 것을 목표로 하는 SE-0337 입니다. 이는 점진적으로 설계되었습니다 : Swift에게 최신 동시성을 염두에 두지 않고 생성되었다는 것을 알리기 위해 @preconcurrency를 사용해서 전체 모듈을 가져올 수 있습니다. 또는 개별적인 클래스, 구조체, 프로퍼티, 메소드 등을 @preconcurrency을 선택적으로 표시할 수 있습니다.

비록 @Sendable Swift에서 가장 까다로운 부분중에 하나이지만, 단기적으로 단기적으로는 큰 규모의 최신 동시성으로 마이그레이션하는 것이 상당히 쉬워집니다.

SE-0327의 결과로 Swift 5.6은 다음과 같이 @StateObject를 사용하는 @MainActor를 인스턴스화 하려고 하는 경우에 경고를 발생시키기 때문에, 다른 변경되는 부분은 actors를 사용하는 것입니다.

import SwiftUI

@MainActor class Settings: ObservableObject { }

struct OldContentView: View {
    @StateObject private var settings = Settings()

    var body: some View {
        Text("Hello, world!")
    }
}

이 경고는 Swift 6에서는 오류로 업그레이드 되므로, 이 코드에서 벗어나 다음 코드를 사용할 준비를 해야 합니다.

struct NewContentView: View {
    @StateObject private var settings: Settings

    init() {
        _settings = StateObject(wrappedValue: Settings())
    }

    var body: some View {
        Text("Hello, world!")
    }
}

WWDC21에서는 특히 @MainActor @StateObject를 사용하는 것이 권장되었기에, 현재 유일한 제대로된 코드가 애플의 자체 문서에서 피하라고 말한 초기화를 사용하는 것을 보는것은 실망스럽습니다.

Swift 5 이전에 더 많은 동시성 변경사항이 인행 중인 점을 감안할때, 현재를 역행하는 것 같은 느낌이 덜 들게끔 정리되었으면 합니다.

Swift 패키지 매니져용 플러그인(Plugins for Swift Package Manager)

Swift 5.6은 외부 빌드 툴을 사용한 플러그인 지원을 시작하는 것을 추가하기 위해 결합된, Swift Package Manager의 기능을 많이(1, 2, 3, 4) 개선하였습니다.

이 시점에 초기 도입자들은 기능이 더 복잡한 이용 사례를 지원할만큼 충분하지 않다고 보고 했지만, 적어도 SwiftGen을 지원하고 DocC와 swift-format이 가능성을 보여주는 추가적인 예제가 있습니다.

이는 분명히 향후 배포에서 매우 빠르게 성장할 것이라 생각됩니다.

Swift 6에 더 가까이(One step closer to Swift 6)

비록 Swift 6이 언제 출시될지 알수 없지만(WWDC 23, 누구 아는사람?) 동시에 Apple은 많은 것을 바꿀(break) 수 있는 큰 기회인것 같습니다. - Swift 3에서 완벽하게 만들어지지 않았거나 5.1과 5.5에서 주요 변경사항이 도입되기 전까지 불가능했던 것들을 정리할 기회입니다.

따라서, 의심할 여지가 없습니다: Swift 6은 여러분의 코드를 망가뜨릴(break) 가능성이 매우 큽니다. 이는 나쁜것이 아니고, 확실히 Apple이 느리고 꾸준한 마이그레이션으로 Swift 3의 전환보다는 훨씬 쉬워질 것입니다. 하지만 향휴 몇년간은 더 많은 튜토리얼이 최신이 아니고, 모범 사례는 아직 없으며, 어느정도 코드가 변동되는 것을 알 수 있습니다.

 

반응형

'Swift > Tip' 카테고리의 다른 글

What’s new in Swift 5.9  (0) 2023.06.12
Array Extension  (0) 2023.05.08
What’s new in Swift 5.7  (0) 2022.08.08
Type의 문자열 이름 사용하기  (0) 2022.04.01
Swift version과 Xcode version  (0) 2022.02.09
What’s new in Swift 5.5  (0) 2022.01.25
What’s new in Swift 5.4  (0) 2021.04.15
What’s new in Swift 5.3  (0) 2021.04.15
Posted by 까칠코더
,