반응형

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

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

What’s new in Swift 5.7

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

Swift 5.7에서는 정규 표현식과 같은 강력한 기능, 삶의 질을 향상시키는 if let과 같은 단축 문법, 그리고 any some 키워드를 일관되게 정리를 포함해서, 언어의 많은(gigantic) 변화와 개선 사항이 있습니다.

이 글에서는 주요 변경사항을 소개하고, 변경 사항을 여러분이 직접 확인할 수 있도록 몇가지 실습 예제를 제공합니다.

  • 중요: 이러한 많은 변경사항은 복잡하고, 많은 부분이 서로 연결되어 있습니다. 최선을 다해서 합리적인 순서로 분리하고, 직접 실습해볼수 있도록 했지만, 너무나 많은 작업을 하다보니 실수한 것을 발견하더라도 놀라지 마세요.

옵셔널 언래핑에 대한 if let 축약법 (if let shorthand for unwrapping optionals)

SE-0345에서는 같은 이름의 새도우(shadowed : 그림자처럼 따라 다니는) 변수로 옵셔널을 언래핑하는것에 대한 새로운 축약 문법을 도입했습니다. 이는 다음과 같은 코드를 작성할 수 있다는 의미입니다.

 var name: String? = "Linda"

if let name {
    print("Hello, \(name)!")
}

이전에는 다음과 같이 작성했을 것입니다.

if let name = name {
    print("Hello, \(name)!")
}

if let unwrappedName = name {
    print("Hello, \(unwrappedName)!")
}   

이러한 변경사항은 객체 내부의 프로퍼티로 확장되지는 않으므로, 다음과 같은 코드는 동작하지 않습니다.

struct User {
    var name: String
}

let user: User? = User(name: "Linda")

if let user.name {
    print("Welcome, \(user.name)!")
}

다중 클로져 타입 추론 구문 (Multi-statement closure type inference)

SE-0326에서 Swift의 클로져에 대한 매개변수와 타입 추론을 사용하는 능력을 획기적으로 개선했으며, 명시적인 입력과 출력 타입을 지정해야 했던 많은 곳을 이제는 제거할 수 있습니다.

이전의 Swift는 사소하지 않은 모든 클로져에 대해서 정말 고군분투 했지만, Swift 5.7부터는 다음과 같은 코드를 작성할 수 있습니다.

let scores = [100, 80, 85]

let results = scores.map { score in
    if score >= 85 {
        return "\(score)%: Pass"
    } else {
        return "\(score)%: Fail"
    }
}

Swift 5.7 이전에는, 다음과 같이, 반환 타입을 명시적으로 지정해줘야 했습니다.

let oldResults = scores.map { score -> String in
    if score >= 85 {
        return "\(score)%: Pass"
    } else {
        return "\(score)%: Fail"
    }
}

시계, 순간, 기간(Clock, Instant, and Duration )

SE-0329에서는 Swift에서의 시간(time)과 기간(duration)을 참조하는 새로 표준화된 방법을 도입했습니다. 이름에서 알수 있듯이, 3가지 주요 구성요소로 나뉩니다.

  • 시계(Clock)는 흘러가는 시간을 측정하는 방법을 나타냅니다. 여기에는 2가지가 내장(built in)되어 있습니다 : 연속적인 시계(continuous clock)는 시스템이 절전모드(asleep) 일때에도 시간을 계속 증가하고, 일시중지하는 시계(suspending clock)는 그렇지 않습니다.
  • 순간(Instant)는 정확한 순간(moment)을 나타냅니다.
  • 기간(Duration)은 2개의 순간(instants)간에 경과된(elapsed) 시간을 나타냅니다.

많은 사람들이 이를 가장 바로 적용할 수 있는 것은 새롭게 업그레이드된 Task API 일것이며, 이제 절전모드(sleep) 시간을 나노초보다 훨씬 더 합리적인 방법으로 지정할 수 있습니다.

try await Task.sleep(until: .now +  .seconds(1), clock: .continuous)

이러한 새로운 API는 허용오차(tolerance)를 지정할 수 있다는 장점도 있으며, 전력의 효율성을 극대화하기 위해 시스템이 절전 마감 시간보다 조금 더 오래 기다릴 수 있습니다. 따라서, 최소 1초 동안 절전모드(sleep)를 원하지만, 총 1.5초까지 지속되길 원하면, 다음과 같이 작성할 수 있습니다.

try await Task.sleep(until: .now + .seconds(1), tolerance: .seconds(0.5), clock: .continuous)

 : 이러한 허용오차(tolerance)는 기본 절전모드(sleep)에만 추가(addition)됩니다 - 시스템은 최소한 1초가 지나기 전에는 절전 모드를 종료하지 않습니다.

아직 발생하지 않았지만, 예전의 나노초 기반의 API는 향후에는 더 이상 사용되지 않을 것입니다.

시계(Clocks)는 일부 특정 작업을 측정하는데 유용하며, 파일을 내보내기에 걸린 시간과 같은 정보를 사용자에게 보여주길 원하는 경우에 유용합니다.

let clock = ContinuousClock()

let time = clock.measure {
    // complex work here
}

print("Took \(time.components.seconds) seconds")

정규 표현식(Regular expressions)

Swift 5.7은 정규 표현식(regexes)와 관련된 여러가지 개선 사항을 도입했고, 문자열을 처리하는 방법을 획기적(dramatically)으로 개선했습니다. 이는 실제로 다음을 포함해서, 상호 연결된 제안(proposals)을 전부 연결(chain)하였습니다.

  • SE-0350은 새로운 Regex 타입을 도입했습니다.
  • SE-0351은 정규 표현식을 생성하는 결과 빌더 방식(builder-powered)의 DSL을도입했습니다.
  • SE-0354 Regex와 문자열을 사용하지 않고 /…/을 사용해서 정규 표현식을 만드는 기능을 추가 했습니다.
  • SE-0357은 정규 표현식을 기반으로 하는 많은 새로운 문자열 처리 알고리즘을 추가 했습니다.

이를 종합하면 다른 언어와 플랫폼과 비교하면 매우 불편했던 부분이었으며, Swift의 문자열은 매우 혁명적인것입니다.

변경사항을 확인하기 위해서, 간단하게 시작해서 위로 올라갑시다.

첫번째로, 다음과 같이, 많은 새로운 문자열 메소드를 그릴수 있습니다.

let message = "the cat sat on the mat"
print(message.ranges(of: "at"))
print(message.replacing("cat", with: "dog"))
print(message.trimmingPrefix("the "))

하지만 이것의 진정한 힘은 모든 정규 표현식을 받아들인다는 것입니다.

print(message.ranges(of: /[a-z]at/))
print(message.replacing(/[a-m]at/, with: "dog"))
print(message.trimmingPrefix(/The/.ignoresCase()))

정규 표현식이 익숙하지 않은 경우에:

  • 첫번째 정규 표현식에서 at이 뒤에 오는 소문자 알파멧 문자와 일치하는 모든 하위 문자열의 범위를 요청함으로써, cat, sat, mat의 위치를 찾을 수 있습니다.
  • 두번째에서는 a부터 m까지만 일치하므로, dog sat on the dog이 출력될 것입니다.
  • 세번째에서는 The를 찾고 있지만, the, THE도 일치하게끔 대소문자를 구분하지 않도록 정규식을 수정했습니다.

이러한 정규식 리터럴(regex literals)을 사용해서 각 정규식이 어떻게 만들어지는지 주목하세요 - 정규식을 /으로 시작하고 끝냄으로써 정규 표현식을 만드는 기능입니다.

정규식 리터럴(regex literals)과 함께, Swift는 이와 유사하게 동작하는 전용(dedicated) Regex 타입을 제공합니다.

do {
    let atSearch = try Regex("[a-z]at")
    print(message.ranges(of: atSearch))
} catch {
    print("Failed to create regex")
}

하지만, 여기에는 코드에서 심각한 부작용(side effects)이 있는 중요한 차이점이 하나 있습니다 : Rege를 사용해서 문자열에서 정규 표현식을 만들때, Swift는 반드시 실행중에 문자열을 분석해서 실제 사용할 표현식을 파악해야 합니다. 이에 비해(In comparision), 정규식 리터럴을 사용하면 Swift는 컴파일 시간에 표현식을 확인할 수 있습니다: 정규식에 오류가 없는지 확인하고, 일치하는 항목이 포함되었는지 정확하게 알수가 있습니다.

이는 매우 주목할만(remarkable) 하기때문에, 반복할 수 있습니다 : Swift는 컴파일 시간에 정규 표현식을 분석해서 유효한지 왁인합니다.

이러한 차이점이 얼마나 강력한지 확인해 보려면 다음 코드를 살펴보세요.

let search1 = /My name is (.+?) and I'm (\d+) years old./
let greeting1 = "My name is Taylor and I'm 26 years old."

if let result = try search1.wholeMatch(in: greeting1) {
    print("Name: \(result.1)")
    print("Age: \(result.2)")
}

이런 종류는 문자열로 만든 정규식으로는 불가능 합니다.

하지만 Swift는 한 단계 더 나아갑니다: 문자열로부터 정규 표현식을 만들수 있고, 정규식 리터럴로부터 만들 수 있지만, SwiftUI 코드와 비슷한 도메인별 언어에서 만들 수 있습니다.

예를들어, My name is Taylor and I’m 26 years old와 동일한 텍스트와 일치시키고자 하는 경우에, 다음과 같이 정규식을 작성할 수 있습니다.

let search3 = Regex {
    "My name is "

    Capture {
        OneOrMore(.word)
    }

    " and I'm "

    Capture {
        OneOrMore(.digit)
    }

    " years old."
}

더 좋은건(Even better), 이러한 DSL 접근법은 발견한 일치하는 항목을 변환시킬 수 있다는 것이고, 만약 Capture 대신에 TryCapture을 사용하는 경우에 캡쳐가 실패하거나 오류가 발생하는 경우에 Swift는 자동으로 전체 정규식이 일치하지 않는 것으로 간주할 것입니다. 따라서, 나이(age)가 일치하는 경우에 나이 문자열을 정수로 변환하기 위해 다음과 같이 작성할 수 있습니다.

let search4 = Regex {
    "My name is "

    Capture {
        OneOrMore(.word)
    }

    " and I'm "

    TryCapture {
        OneOrMore(.digit)
    } transform: { match in
        Int(match)
    }

    Capture(.digit)

    " years old."
}

그리고 다음과 같이 특정 타입의 변수를 사용해서 해당 타입과 일치하는 항목을 함께 가져올수도 있습니다.

let nameRef = Reference(Substring.self)
let ageRef = Reference(Int.self)

let search5 = Regex {
    "My name is "

    Capture(as: nameRef) {
        OneOrMore(.word)
    }

    " and I'm "

    TryCapture(as: ageRef) {
        OneOrMore(.digit)
    } transform: { match in
        Int(match)
    }

    Capture(.digit)

    " years old."
}

if let result = greeting.firstMatch(of: search5) {
    print("Name: \(result[nameRef])")
    print("Age: \(result[ageRef])")
}

세가지 옵션중에, 정규식 리터럴이 가장많이 사용될것이라 예상되지만, Swift 6이 출시될때까지 기본적으로 지원되지 않을 것으로 보입니다 - 이 구문을 사용하려면 Xcode의 Other Swift Flags 설정에 -Xfrontend -enable-bare-slash-regex를 추가하세요.

기본 표현식에서 타입 추론하기(Type inference from default expressions)

SE-0347은 제네릭 매개변수 타입과 기본 값을 사용해서 Swift 기능을 확장합니다. 이를 허용하는 것은 꽤 편해보이지만(niche), 중요합니다. 제네릭 타입이나 함수가 있는 경우에, 이전의 Swift에서는 컴파일 오류를 발생하는 곳에서 기본 표현에 대한 구체적인(concrete) 타입을 제공할 수 있습니다.

예를 들어, 모든 종류의 시퀀스(sequence)에서 임의의 count 수를 반환하는 함수가 있을 것입니다.

func drawLotto1<T: Sequence>(from options: T, count: Int = 7) -> [T.Element] {
    Array(options.shuffled().prefix(count))
}

이름 배열이나 정수 범위와 같은, 모든 종류의 시퀀스를 사용해서 복권(lotter)를 실행할 수 있습니다.

print(drawLotto1(from: 1...49))
print(drawLotto1(from: ["Jenny", "Trixie", "Cynthia"], count: 2))

SE-0347은 함수의 T 매개변수에 대한 기본값으로 구체적인 타입을 제공하도록 이를 확장했으며, 문자열 배열이나 모든 다른 종류의 컬렉션을 유연하게 사용할 수 있도록 하면서, 대부분의 경우에 원하는 범위 옵션을 기본 값으로 설정할 수 있습니다.

func drawLotto2<T: Sequence>(from options: T = 1...49, count: Int = 7) -> [T.Element] {
    Array(options.shuffled().prefix(count))
}

그리고 이제 우리는 사용자정의 시퀀스를 사용해서 함수를 호출하거나, 기본 값을 그대로 사용할 수 있습니다.

print(drawLotto2(from: ["Jenny", "Trixie", "Cynthia"], count: 2))
print(drawLotto2())

상위 레벨 코드에서의 동시성(Concurrency in top-level code)

SE-0343은 상위 레벨 코드에 대한 Swift의 지원을 업그레이드 해서 바로 동시성을 지원하는 것입니다. - macOS Command Line Tool 프로젝트에서의 main.swift를 생각해보세요. 이러한 변화가 겉으로 봤을때는 다소 사소해(trivial) 보일지 모르겠지만, 실현(happy) 하기 위해서 많은 일(work) 했습니다(took).

실제로(In practice), 이는 다음과 같은 코드를 main.swift 파일에 직접 작성할 수 있음을 의미합니다.

let url = URL(string: "https://hws.dev/readings.json")!
let (data, _) = try await URLSession.shared.data(from: url)
let readings = try JSONDecoder().decode([Double].self, from: data)
print("Found \(readings.count) temperature readings")

이전에는, 비동기 main() 메소드를 사용하는 새로운 @main 구조체를 만들어야 했기에, 이러한 새로운 간단한 접근법은 크게 개선되었습니다.

불투명한 매개변수 선언(Opaque parameter declarations)

SE-0341은 간단한 제네릭이 사용되는 곳에서 some을 매개변수 선언에서 사용할 수 있는 기능을 해제(unlocks)하였습니다.

예를들어, 배열이 정렬되었는지 확인하는 함수를 작성하는 경우에, Swift 5.7 이후에서는 다음과 같이 작성할 수 있습니다.

func isSorted(array: [some Comparable]) -> Bool {
    array == array.sorted()
}

[some Comparable] 매개변수 타입은 해당 함수가 Comparable 프로토콜을 준수하는 타입 요소를 포함하는 배열로 동작하는 것을 의미하며, 제네릭 코드를 이해하기 쉽게 표현(syntactic sugar)한 것과 같습니다.

func isSortedOld<T: Comparable>(array: [T]) -> Bool {
    array == array.sorted()
}

물론, 더 길게 제한된(constrained) 확장을 작성할수도 있습니다.

extension Array where Element: Comparable {
    func isSorted() -> Bool {
        self == self.sorted()
    }
}

더 단순한 제네릭 구문은 합성된 제네릭 매개변수에 대한 특정 이름이 없기 때문에, 타입에 더 복잡한 제약을 추가할 수 없다는 것을 의미합니다.

중요 : API를 변경하지(breaking) 않고 명시적인 제네릭 매개변수와 더 간단해진 새로운 구문(syntax) 간에 전환 할 수 있습니다.

구조적 불투명 결과 타입(Structural opaque result types)

Se-0328은 불투명한 결과 타입이 사용되는 곳의 범위가 넓어집니다.

예를들어, 이제는 한 번에 하나 이상의 불투명한 타입을 반환할 수 있습니다.

func showUserDetails() -> (some Equatable, some Equatable) {
    (Text("Username"), Text("@twostraws"))
}

물론 불투명한 타입들도 반환 할수 있습니다.

func createUser() -> [some View] {
    let usernames = ["@frankefoster", "@mikaela__caron", "@museumshuffle"]
    return usernames.map(Text.init)
}

또는 호출될때 불투명한 타입을 반환하는 함수를 다시 돌려 보낼수도 있습니다.

func createDiceRoll() -> () -> some View {
    return {
        let diceRoll = Int.random(in: 1...6)
        return Text(String(diceRoll))
    }
}

따라서, Swift 언어를 어울리게(harmonizing) 만들어서 모든것을 일관되도록 하기 위한 또 다른 훌륭한 예제입니다.

모든 프로토콜에 대한 기존 잠금 해제(Unlock existentials for all protocols)

SE-0309는 Swift가 Self 또는 연관된 타입(associated)이 필요할때 프로토콜을 타입처럼 사용을 금지하는 것을 대폭(significantly) 완화(loosens)해서, 특정 프로퍼티 또는 메소드에 따라 제한 없는 모델로 변경하였습니다.

간단히 말해서, 이는 다음에 오는 코드가 합법화 되는 것을 의미합니다.

let firstName: any Equatable = "Paul"
let lastName: any Equatable = "Hudson"

Equatable Self가 필요한 프로토콜이며, 이를 채택(adopts)하는 특정 타입을 참조하는 기능을 제공하는 것을 의미합니다. 예를들어, Int Equatable을 준수(conforms)하므로, 4 == 4라고 말할때, 실제로 2개의 정수를 받아들이고, 그것들이 일치하면 true를 반환하는 함수를 실행합니다.

Swift는 비슷한 함수를 사용해서 func ==(first: Int, second: Int) -> Bool의 기능을 구현할 수 있지만, 확장성이 좋지는 않습니다. - Booleans, 문자열, 배열, 등을 처리하기 위해 많은 함수들이 필요합니다. 대신 Equatable 프로토콜은 다음과 같은 것을 필요합니다 : func –(lhs: Self, rhs: Self) -> Bool. 영어로, 같은 타입의 인스턴스 2개를 받아서 같은지를 알려줘야 합니다를 의미합니다. 2개의 정수, 2개의 문자열, 2개의 Booleans, 또는 Equatable를 준수하는 다른 모든 타입 2개일 수 있습니다.

이와 비슷한 문제를 피하기 위해, Self가 Swift 5.7 이전의 프로토콜에 나타날때마다 컴파일러는 다음과 같은 코드를 사용하는 것을 허용하지 않았습니다.

let tvShow: [any Equatable] = ["Brooklyn", 99]

Swift 5.7 이후부터는(onwards), 이 코드가 허용되고, 이제 이러한 제한(restrictions)은 실제로 제한을 적용해야 하는 곳에서 타입을 사용하려고 시도하는 곳으로 밀려나게 됩니다. 이는 ==은 2개의 같은 타입의 인스턴스가 있어야 하며, any Equatable을 사용해서 데이터의 정확한 타입을 숨기기 때문에, firstName == lastName이라 작성할 수 없다는 것을 의미합니다.

하지만, 우리가 얻은 것은 데이터에 대해 실시간으로 구체적으로 어떤 작업을 하는지 확인할 수 있는 능력입니다. 혼합된 배열의 경우에, 다음과 같이 작성할 수 있습니다.

for parts in tvShow {
    if let item = item as? String {
        print("Found string: \(item)")
    } else if let item = item as? Int {
        print("Found integer: \(item)")
    }
}

또는 2개의 문자열의 경우에, 다음과 같이 사용할 수 있습니다.

if let firstName = firstName as? String, let lastName = lastName as? String {
    print(firstName == lastName)
}

이러한 변경사항이 무엇을 하는지를 이해해야하는 핵심은 타입의 내부에 대해서 특별히 알아야하지 않는 한, 이 프로토콜을 좀 더 자유롭게 사용하도록 해주는 것을 기억하는 것이며, 따라서, 어떤 시퀀스의 모든 항목이 Identifiable 프로토콜을 준수하는지 확인하기 위해 코드를 작성할 수 있습니다.

func canBeIdentified(_ input: any Sequence) -> Bool {
    input.allSatisfy { $0 is any Identifiable }
}

연관된 기본 타입에 대해 같은 타입의 가벼운 요구사항(Lightweight same-type requirements for primary associated types)

SE-0346은 특별한 연관된 타입(associated types)이 있는 프로토콜을 참조하기 위한 새롭고, 간단한 구문을 추가합니다.

예를 들어, 다른 종류의 데이터를 다른 방식으로 캐시(cache)하기 위한 코드를 작성하는 경우에, 다음과 같이 시작할 것입니다.

protocol Cache<Content> {
    associatedtype Content

    var items: [Content] { get set }

    init(items: [Content])
    mutating func add(item: Content)
}

프로토콜이 이제 프로토콜과 제네릭 타입 둘다 같아 보이는 것에 주목하세요 - 연관된 타입 선언하는

준수하는 타입으로 채워야하는 어떤 종류의 구멍(hole)을 선언하는 연관된 타입을 가지고 있지만, 해당 타입을 각 꺾인 괄호 안에 나열합니다(list): Cache.

꺽인 괄호(angle brackets) 안쪽 부분은 Swift가 연관된 기본 타입(primary associated type)이라는 하는 부분이고, 모든 연관된 타입이 선언되야 하는게 아니라는 것을 이해하는 것이 중요합니다. 대신, 호출하는 코드가 특별히 신경쓰는 것들, 예를들어, Identifiable 프로토콜에서 딕셔너리 키와 값의 타입 또는 식별자 타입들만만 나열해야(list) 합니다. 이 경우에 캐시의 컨텐츠가(문자열, 이미지, 사용자, 등) 연관된 기본 타입이라고 했습니다.

이 시점에, 우리는 계속해서 이전처럼 프로토콜을 사용할 수 있습니다 - 캐시할 데이터를 만들수도 있고, 다음과 같이 프로토콜을 준수하는 구체적인 캐시 타입을 만들수 있습니다.

struct File {
    let name: String
}

struct LocalFileCache: Cache {
    var items = [File]()

    mutating func add(item: File) {
        items.append(item)
    }
}

이제 똑똑한 부분입니다: 캐시를 생성할때, 다음과 같이 특정 항목을 직접 생성할 수 있습니다.

func loadDefaultCache() -> LocalFileCache {
    LocalFileCache(items: [])
}

하지만 다음과 같이 우리가 하는 것을 숨기고자 할때가 자주 있습니다.

func loadDefaultCacheOld() -> some Cache {
    LocalFileCache(items: [])
}

some Cache를 사용하면 특정 캐시가 반환되는지에 대한 우리의 생각을 바꿀수 있는 유연함이 있지만, SE-0346을 통해서 완전한 구제척인 타입과 불투명한 반환 타입간에 중간지점을 제공합니다. 따라서, 다음과 같이 프로토콜을 사용할 수 있습니다.

func loadDefaultCacheNew() -> some Cache<File> {
    LocalFileCache(items: [])
}

따라서, 향후에 다른 Cache를 준수하는 타입으로 바꿀수 있는 기능을 가지고 있지만, 여기에서 선택하는 것이 무엇이든 내부적인 파일로 저장될 것임을 분명이 했습니다.

똑똑한 구문은 확장(extnsions)을 포함해서 다른곳에서도 확장됩니다.

extension Cache<File> {
    func clean() {
        print("Deleting all cached files…")
    }
}

제네릭 제약조건:

func merge<C: Cache<File>>(_ lhs: C, _ rhs: C) -> C {
    print("Copying all files into a new location…")
    // now send back a new cache with items from both other caches
    return C(items: lhs.items + rhs.items)
}

하지만 무엇보다 가장 도움이 되는 것은 SE-0358이 주요(primary) 연관된 타입을 Swift의 표준 라이브러리에서 가져오므로, Sequence, Collection, 등이 혜택을 받습니다 - Sequence을 사용해서, 사용되는 시퀀스 타입에 관계없이 코드를 작성할 수 있습니다.

제한된 기존 타입(Constrained existential types)

SE-0353 SE-0309 SE-0346을 구성해서 any Sequence와 같은 코드를 작성하는 기능을 제공합니다.

이는 그 자체로 큰 특징이지만, 구성요소 부분을 이해하고나서 모든것이 어떻게 어울리는지 확인 할 수 있기를 바랍니다!

분산된 actor 격리(Distributed actor isolation)

SE-0336 SE-0344은 actors가 분산된 형태로 동작하는 기능을 소개합니다 - 원격 프로시져 호출(RPC: remote procedure calls)을 사용해서 네트워크로 프로퍼티를 읽고 쓰거나 메소드를 호출합니다.

이는 상상할 수 있는 모든 복잡한 문제이지만, 더 쉽게 만들어 주는 3가지가 있습니다.

  1. 위치 투명서(location transparency)의 Swift의 접근 방법은 사실상 actor가 원격에 있다고 가정하고, 실제로 컴파일할때 actor가 지역이나 원격인지 여부를 결정하는 방법을 제공하지 않습니다 - 무슨 일이 있어도 똑같은 await 호출을 사용하고, actor가 로컬인 경우에 일반 로컬 actor 함수처럼 처리 됩니다.
  2. Apple은 actor 전송(transport) 시스템을 빌드하도록 강요하기 보다는, 우리가 사용할 수 있도록 이미 만들어진 구현(ready-made implementation) 제공합니다. Appl은 몇가지 좋은(mature) 구현만이 사용될 것이라 예상합니다.라고 말했지만(said), Swift에서 분산된(distributed) actor 기능들은 사용하는 actor 전송에 영향을 받지 않습니다.
  3. actor에서 분산된 actor로 넘어가려면, 대부분 필요에 따라서 distributed actor 를 작성하고나서 distributed func가 필요합니다.

따라서, 카드 거래 시스템을 추적하는 하는 것을 시뮬레이션하기 위해, 다음과 같이 코드를 작성할 수 있습니다.

// use Apple's ClusterSystem transport 
typealias DefaultDistributedActorSystem = ClusterSystem

distributed actor CardCollector {
    var deck: Set<String>

    init(deck: Set<String>) {
        self.deck = deck
    }

    distributed func send(card selected: String, to person: CardCollector) async -> Bool {
        guard deck.contains(selected) else { return false }

        do {
            try await person.transfer(card: selected)
            deck.remove(selected)
            return true
        } catch {
            return false
        }
    }

    distributed func transfer(card: String) {
        deck.insert(card)
    }
}

분산된 actor 호출의 오류 발생하는 특성(nature)때문에, person.transfer(card:) 호출이 오류가 발생하는 경우에, 하나의 컬렉션에서 카드를 제거하는 것이 안전하다는 것을 확신할 수 있습니다.

Swift의 목표는 actor에 대한 지식을 분산된 actors에게 매우 쉽게 전달할 수 있지만, 주의를 끄는 몇가지 중요한 차이점이 있습니다.

첫번째, 네트워크 호출이 잘못되어 실패가 발생할수 있기 때문에, 해당 함수가 오류가 발생한다고 표시되지 않았더라도, 모든 분산된 함수들은 반드시 try await를 사용해서 호출해야 합니다.

두번째, Codable처럼, 분산된 메소드의 모든 매개변수와 반환 값은 반드시 선택한 직렬 프로세스를 준수해야합니다. 이는 컴파일할때 확인되므로, Swift는 원격 actors로 부터 데이터늘 보내고 받을 수 있다는 것을 보장할 수 있습니다.

세번째, 데이터 요청을 최소화하기 위해서 actor API 를 조정하는 것을 고려해야 합니다. 예를들어, 분산된(destributed) actors의 username, firstName, lastNmae프로퍼티를 읽고자하는 경우에, 네트워크를 통해서 여러번 왔다갔다 하는 것을 피하기 위해서 개별 프로퍼티를 요청하는 것보다는 하나의 메소드 호출로 3개 모두를 요청하는 것을 선호해야합니다.

결과 빌더를 위한 buildPartialBlock(buildPartialBlock for result builders)

SE-0348는 복잡한 결과 빌더 구현하기 위해 필요한 오버로드(overloads)를 극적으로 단순화하며, 이는 Swift의 고급 정규 표현식(regular expression) 지원이 가능한 이유입니다. 그러나, 이론적으로 다양한 제네릭을 추가하지 않고도 SwiftUI의 10개 뷰 제한을 제거하므로, SwiftUI 팀에서 채택되는 경우에, 많은 사람들이 행복할 것입니다.

실용적이 예를들어, 다음은 SwiftUI ViewBuilder의 간단한 버젼입니다.

@resultBuilder
struct SimpleViewBuilderOld {
    static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View {
        TupleView((c0, c1))
    }

    static func buildBlock<C0, C1, C2>(_ c0: C0, _ c1: C1, _ c2: C2) -> TupleView<(C0, C1, C2)> where C0: View, C1: View, C2: View {
        TupleView((c0, c1, c2))
    }
}

buildBlack()의 2가지 버젼을 포함하도록 만들었습니다: 하나는 2개의 뷰를 허용하고 다른 하나는 3개를 허용합니다. - 실제로, SwiftUI는 다양한 대안을 허용하지만, 결정적으로 최대 10개까지 입니다. - 다음은 TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>를 반환하는 buildBlack() 이지만, 실제적으로 그 이상은 없습니다.

다음과 같이 함수나 계산된 프로퍼티(computed properties)로 결과 빌더(result builder)를 사용할 수 있습니다.

@SimpleViewBuilderOld func createTextOld() -> some View {
    Text("1")
    Text("2")
    Text("3")
}

buildBlock() 변형(variant)을 사용하는 3개의 Text 뷰 모두를 허용할 것이고, 이러한 모든것들을 포함하는 하나의 TupleView을 반환할 것입니다. 하지만, SwiftUI가 11개 이상을 지원하지 않는 것과 같은 방법으로 더 이상 오버로드(overloads)를 제공하지 않기 때문에, 이 단순화된 예제에서는 4번재 Text뷰를 추가할 방법이없습니다.

이는 시퀀스의 reduce() 메소드 처럼 동작하기 때문에, 새로운 buildPartialBlock()이 나오게 되었습니다: 하나의 초기값을 가지고, 다음에 오는 모든 값에 이미 가지고 있는 값을 추가해서 없데이트 합니다.

따라서, 하나의 뷰를 받아들이는 방법과 해당 뷰를 다른 뷰와 결합하는 방법을 알고 있는 새로운 결과 빌더(result builder)를 생성할 수 있습니다.

@resultBuilder
struct SimpleViewBuilderNew {
    static func buildPartialBlock<Content>(first content: Content) -> Content where Content: View {
        content
    }

    static func buildPartialBlock<C0, C1>(accumulated: C0, next: C1) -> TupleView<(C0, C1)> where C0: View, C1: View {
        TupleView((accumulated, next))
    }
}

하나 또는 두개의 뷰를 허용하는 변형(variants) 만 있지만, 누적(accumulate)되기 때문에 원하는 만큼 사용할 수 있습니다.

@SimpleViewBuilderNew func createTextNew() -> some View {
    Text("1")
    Text("2")
    Text("3")
}

하지만 결과는 같지 않습니다: 첫번째 예제에서 TupleView를 반환하지만, 지금은 TupleView<(TupleView<(Text, Text)>, Text)>를 반환합니다 - 하나의 TupleView가 다른 하나의 내부에 중첩됩니다. 다행히도, SwiftUI 팀이 이를 채택하려는 경우에 이전과 같은 19개의 buildPartialBlock()을 생성할 수 있어야 하며, 지금 명시적으로 사용하는 것처럼, 컴파일러가 자동으로 10개의 그룹을 생성해야 합니다.

: buildPartialBlock()은 모든 플랫폼별 런타임과는 다르게, Swift의 일부이므로, 이를 채택하는 경우에 이전 OS 릴리즈로 다시 배포되는 것을 확인할 수 있습니다.

암시적으로 열린 실체(Implicitly opened existentials)

SE-0352는 Swift가 많은 상황에서 프로토콜을 사용해서 제네릭 함수를 호출하도록 하며, 이전에 있었던 약간(somewhat) 이상한 것(odd barrier)들을 제거합니다.

예제에서 처럼, 다음은 모든 Numberic 값으로 작업할 수 있는 하나의 제네릭 함수가 있습니다,

func double<T: Numeric>(_ number: T) -> T {
    number * 2
}

예를 들어 double(5)를 직접 호출하고, Swift 컴파일러는 해당 함수를 특별히 선택할 수 있습니다. - 성능상의 이유로, 실제로 Int를 허용하는 버젼을 만듭니다.

하지만, SE-0352는 데이터가 다음과 같은 프로토콜을 준수하는 것을 알고 있을때 그 함수를 호출하도록 허용해주는 것입니다.

let first = 1
let second = 2.0
let third: Float = 3

let numbers: [any Numeric] = [first, second, third]

for number in numbers {
    print(double(number))
}

Swift는 이러한 실체 타입(existential types)을 호출합니다: 사용중인 실제 데이터 타입은 상자 안에 있고, 상자에서 메소드를 호출할때, Swift는 상자 안에 있는 데이터에 대한 메소드를 암시적으로 호출해야 하는 것으로 알고 있습니다. SE-0352는 동일한 능력을 함수 호출로 확장합니다. 반복문 안의 number 값은 하나의 실체 타입(existential type) 이지만(Int, Double, Float이 들어있는 상자), Swift는 상자 안에 있는 값을 전송해서 제네릭 double() 함수에 전달할 수 있습니다.

이는 한계가 있고, 당연하다 생각합니다. 예를들어, 다음과 같은 코드는 동작하지 않습니다.

func areEqual<T: Numeric>(_ a: T, _ b: T) -> Bool {
    a == b
}

print(areEqual(numbers[0], numbers[1]))

Swift는 ==를 사용해서 2개의 값을 비교할 수 있는 값인지 정적(즉, 컴파일시)으로 확인할 수 없으므로, 코드가 빌드 되지 않습니다.

Swift 스니펫(Swift snippets)

SE-0356은 프로젝트의 작지만 중요한 문서 격차를 채우기 위해 설계된, 스니펫(snippets)의 개념을 도입했습니다: 샘플 코드는 간단한 API문서보다 크지만, 프로젝트에서 한가지 특징을 보여주도록 설계된, 예제 프로젝트 보다는 더 작습니다.

겉으로 보기에 충분히 간단해 보이고, 특정 문제에 대한 해결책이나 하나의 API를 보여주는 스니펫을 제공하는 것을 상상할수 있지만, 스니펫이 특별히 흥미롭게 하는 다음 3가지가 있습니다.

  1. 주석 내부에 작은 양의 특수 마크업(markup)을 넣어서 스니펫을 그리는 것을 조정할 수 있습니다.
  2. 커맨드 라인(command line)에서 쉽게 빌드하고 실행할 수 있습니다.
  3. DocC에서 아름답게 통합되고, 나머지 문서와 함께 그려집니다.

2가지 형식의 특수 마크업(markup)이 있습니다: 각 스니펫에 대한 짧은 설명을 만들기 위해 //!을 사용하고, // MARK: Hide // MARK: Show을 사용해서 보이지 않는 코드 블록을 만들수 있으며, 스니펫이 보여주려는 것과 특별히 관련 없는 작업을 할때 사용할 수 있습니다.

다음과 같이 스니펫을 생성할 수 있습니다.

//! Demonstrates how to use conditional conformance
//! to add protocols to a data type if it matches
//! some constraint.

struct Queue<Element> {
    private var array = [Element]() 
    // MARK: Hide

    mutating func append(_ element: Element) {
        array.append(element)
    }

    mutating func dequeue() -> Element? {
        guard array.count > 0 else { return nil }
        return array.remove(at: 0)
    }
    // MARK: Show
}

extension Queue: Encodable where Element: Encodable { }
extension Queue: Decodable where Element: Decodable { }
extension Queue: Equatable where Element: Equatable { }
extension Queue: Hashable where Element: Hashable { }

let queue1 = Queue<Int>()
let queue2 = Queue<Int>()
print(queue1 == queue2)

// MARK: Hide // MARK: Show를 사용해서 새부 구현을 숨기고, 중요한 부분에 집중할 수 있도록 합니다.

커맨드 라인(command-line) 지원에 대해, 3가지 새로운 명령 변형(variations)을 실행할 수 있습니다.

  • swift build –build-snippets는 모든 스니펫을 포함한 소스 타겟을 빌드합니다 #스니펫이 포함된 소스 타겟을 빌드합니다.
  • swift build SomeSnippet는 SomeSnippet.swift를 독립 실행하는 것처럼 빌드합니다.
  • swift run SomeSnippet는 SomeSnippet.swift를 즉시 실행합니다.

생성한 각 스니펫(snippet)은 패키지에 남아있는 생성한 모든 코드에 접근할 수 있으며, 프로젝트의 다양한 부분에 대한 예제 코드를 제공하는 환상적인 방법임을 의미합니다.

비동기 속성을 사용할 수 없음(Unavailable from async attributed)

SE-0340은 Swift의 동시성 모델에서 잠재적으로 위험한 부분을 막아주며, 비동기 컨텍스트에서 타입과 함수가 문제가 될수 있기에, 사용할 수 없도록 표시할 수 있습니다. 스레드-로컬 저장소, 잠금, 뮤텍스, 세마포어 등을 사용하지 않는한 이 속성을 사용할 가능성은 낮지만, 사용하는 코드를 호출할 수 있으므로, 적어도 이런게 존재한다는 것을 알고 있을 가치가 있습니다.

비동기 컨텍스트에서 사용할수 없음을 표시하기 위해서, 일반 플랫폼 선택과 함께 @available을 사용하고, 끝부분에 nosync를 추가합니다. 예를들어, 모든 플랫폼에서 동작하는 함수이지만, 비동기 호출할때 문제가 될수 있으면, 다음과 같이 표시합니다.

@available(*, noasync)
func doRiskyWork() {

}

그리고나서, 동기 함수에서 정상적으로 호출할 수 있습니다.

func synchronousCaller() {
    doRiskyWork()
}

하지만, 비동기 함수에서 동일하게 시도하는 경우에, Swift는 오류를 발생할 것이므로, 다음 코드는 동작하지 않을 것입니다.

func asynchronousCaller() async {
    doRiskyWork()
}

이러한 보호는 현재 상황에 대해서 개선된 것이지만, 다음과 같이 noasync 함수를 호출을 중단하지 않기 때문에, 지나치게 의존해서는 안됩니다.

func sneakyCaller() async {
    synchronousCaller()
}

비동기 컨텍스트에서 실행하지만, 동기 함수를 호출하며, noasync 함수 doRiskWork()를 호출할 수 있습니다.

noasync가 개선되었지만, 여전히 사용할때 주의해야 합니다. 다행히도 Swift Evolution 제안에서 말했듯이 속성은 상당히 제한된 특수 사용하는 상황에서만 사용될것으로 예상됩니다 - 이 속성을 사용하는 코드를 보지 못할 가능성이 큽니다.

잠깐… 더 있습니다!(But wait… there’s more!)

이 시점에 모든 변경사항들로 머리가 어지러울 것이라 예상되지만, 다루지 않는 것이 더 있습니다.

많은 변화가 있고, 그 중 일부는 실제로 프로젝트를 멈추게 할 것입니다. 따라서 너무 많이 멈추는 것을 피하기 위해서 Swift팀은 Swift 6이 출시 될때까지 이러한 변경사항들 중 일부를 활성화 하는 것을 연기하는 것을 결정했습니다 - 이러한 변경사항이 적용되지만 -enable-bare-slash-regex와 같이 컴파일 플래그를 사용해서 활성화 해야 할 수도 있습니다.

반응형

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

What’s new in Swift 5.8  (0) 2023.06.14
What’s new in Swift 5.9  (0) 2023.06.12
Array Extension  (0) 2023.05.08
Type의 문자열 이름 사용하기  (0) 2022.04.01
What’s new in Swift 5.6  (0) 2022.03.21
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
Posted by 까칠코더
,