Swift

Swift 5.9 슬쩍 맛보기 (feat. WWDC 2023)

GREEN.1229 2023. 6. 9. 15:41

안녕하세요. 그린입니다🍏

이번 포스팅에서는 Swift 5.9가 소개되면서 새로 나온것들에 대해 살펴보겠습니다🙋🏻

 

전체적으로 Swift 개발 프로젝트 방식의 업데이트부터 언어적으로 매크로 기능도 이번에 생겼고 성능 및 안전도도 많이 향상되었다고해요.

저는 이번 포스팅에서는 중점적으로 Swift 언어에만 포커싱을 맞춰 한번 볼까합니다.

 

if 및 switch 표현

변수를 초기화 할때 이제는 아래와 같이 if 및 switch 조건문을 사용할 수 있습니다.

즉, 코드를 줄이는데 많은 도움을 줍니다👍

// if
let score = 800
let simpleResult = if score > 500 { "Pass" } else { "Fail" }
print(simpleResult)

// switch
let complexResult = switch score {
    case 0...300: "Fail"
    case 301...500: "Pass"
    case 501...800: "Merit"
    default: "Distinction"
}

그럼 이제 메서드에서 요런 표현을 통해 return을 주지 않아도 가능해지죠!

func rating(for score: Int) -> String {
    switch score {
    case 0...300: "Fail"
    case 301...500: "Pass"
    case 501...800: "Merit"
    default: "Distinction"
    }
}

 

보다 정확한 컴파일

SwiftUI에서 간혹 뷰를 그리면서 사용하면서 컴파일 에러를 맞이할때 그 에러 위치가 모호한 경우가 많았습니다.

그런데 이에 대해 컴파일러가 조금 더 발전되었습니다.

즉 어떻게 보면 resultBuilder를 더 잘 파악하고 어디가 원인인지 그 위치에 컴파일 에러를 던져주게 되었죠.

요런식으로 근본적인 위치를 어느정도 정확하게 표시해줍니다.

 

Value & Type Prameter Packs

기존 시스템 인수 길이에 대한 제한이 있어 힘든 부분이 있었습니다.

즉, 제네릭으로 갯수를 지정해주면 그 외 더 오버되면 문제가 발생했죠.

이제는 파라미터 팩이라는 기능이 생겨 좀 더 편리해졌어요.

func pairUp2<each T, each U>(firstPeople: repeat each T, secondPeople: repeat each U) -> (repeat (first: each T, second: each U)) {
    return (repeat (each firstPeople, each secondPeople))
}

<each T>와 같은 형태로 each를 붙여줌으로 파라미터 팩 유형을 만듭니다.

repeat each T 타입 선언으로 사실상 실제 값으로 확장합니다.

다시 이 두 제네릭을 튜플로 짝을 이루게합니다.

zip 함수를 이용해 구현해냈다고 하네요.

 

즉 이제는 이렇게 제네릭을 가져감으로 갯수에 대해 스트레스를 받지 않아도 됩니다!

 

Swift 5.9 이전 SwiftUI만 생각해보더라도 뷰 빌더에서는 최대 10개까지만 결합할 수 있었는데 이제는 제한이 없어졌으니 조금 더 자유롭게 큰 신경을 쓰지 않고 구성할 수 있겠네요👍

 

Macro

이번 Swift 5.9 업데이트에서 가장 매우 대단히 중요한 업데이트입니다!

바로 이 매크로 하나 구현하고 SwiftUI나 SwiftData 등 사실상 모든곳에서 매크로를 사용함으로써 이점을 많이 가져간것 같아요 🙌

그렇기에 이 매크로는 꼭 한번 파봐야될 부분인것 같습니다.

 

매크로를 추가해 컴파일 타임에 구문을 변환하는 코드를 생성할 수 있죠.

다만 매크로를 사용할때 주의해야할 점이 있다고 합니다.

 

1️⃣ 단순한 문자열 대체가 아니라 type-safe 하기에 매크로에서 어떤 데이터로 작동할지 명확하게 알려줘야 합니다.

2️⃣ 빌드 단계에서 외부 프로그램으로 실행되며 기본 앱 대상에서는 실행되지 않습니다.

3️⃣ ExpressionMacro, AccessorMacro, ConformanceMacro와 같이 여러개의 타입으로 나뉩니다.

4️⃣ 매크로는 파싱된 소스 코드와 함께 작동하며 개별적인 부분들을 쿼리할 수 있습니다.

5️⃣ sandbox 내에서만 작동하며 주어진 데이터에서만 작동해야 합니다.

 

매크로는 소스 코드를 이해하고 조작하기 위해 SwiftSyntax 라이브러리를 중심으로 구축되었기에 이걸 매크로에 대한 종속성으로 추가해줘야 합니다.

 

그럼 간단한 매크로를 살펴볼까요?

 

public struct BuildDateMacro: ExpressionMacro {
    public static func expansion(
        of node: some FreestandingMacroExpansionSyntax,
        in context: some MacroExpansionContext
    ) -> ExprSyntax {
        let date = ISO8601DateFormatter().string(from: .now)
        return "\"\(raw: date)\""
    }
}

앱이 빌드된 날짜 / 시간을 반환하는 매크로입니다.

해당 매크로는 기본 앱 타겟에 있으면 안됩니다.

 

이제 매크로를 만든 동일한 모듈 내에서 CompilerPlugin 프로토콜을 따르는 구조체를 만들어 매크로를 내보냅니다.

import SwiftCompilerPlugin
import SwiftSyntaxMacros

@main
struct MyMacrosPlugin: CompilerPlugin {
    let providingMacros: [Macro.Type] = [
        BuildDateMacro.self
    ]
}

 

마지막으로 Package.swift에서 해당 모듈을 추가합니다.

.macro(
  name: "MyMacrosPlugin",
  dependencies: [
    .product(name: "SwiftSyntax", package: "swift-syntax"),
    .product(name: "SwiftSyntaxMacros", package: "swift-syntax"),
    .product(name: "SwiftCompilerPlugin", package: "swift-syntax")
  ]
),

 

이렇게 되면 외부 모듈에서 매크로 생성이 완료된것입니다.

 

이제 가져온 매크로를 사용해볼 수 있습니다.

 

우선 매크로를 정의해야 합니다.

@freestanding(expression)
macro buildDate() -> String =
  #externalMacro(module: "MyMacrosPlugin", type: "BuildDateMacro")

모듈 내부에 존재하게 위치하며 위에서 만든 매크로의 모듈과 타입을 넣어주면서 문자열을 반환하도록 매크로를 만듭니다.

 

그 다음 실제 사용은 아래와 같이 쉽게 할 수 있습니다.

print(#buildDate)

 

매크로에 대해 더 자세히 파고 들어보려면 WWDC 2023의 아래 두 섹션을 봐야할것 같네요🫠

 

Expand on Swift macros
Write Swift macros

 

Noncopyable structs & enums

복사할 수 없는 구조체 및 열거형의 개념을 도입하여 해당 단일 인스턴스를 여러곳에서 공유할 수 있습니다.

즉, 인스턴스는 하나지만 여러 부분에서 액세스 할 수 있죠.

struct User: ~Copyable {
    var name: String
}

~Copyable을 통해 복사할수 없음을 의미해줍니다.

 

자 그럼 이렇게 복사할 수 없는 타입으로 만들어줬는데 아래 코드를 볼까요?

func createUser() {
    let newUser = User(name: "Anonymous")

    var userCopy = newUser
    print(userCopy.name)
}

createUser()

그럼 newUser의 복사본인 userCopy를 가져올 수 있을까요?

답은 NO 입니다🥲

값이 이미 소비가 되었기 때문이죠.

즉 소유권이 이미 newUser에 가버렸기에 더 이상 사용할 수 없습니다.

컴파일 에러를 맞이하게 되죠.

 

조금 더 구체적으로 볼까요?

 

struct FileDescriptor: ~Copyable {
    private var fd: CInt
  
    init(descriptor: CInt) { self.fd = descriptor }

    func write(buffer: [UInt8]) throws {
        let written = buffer.withUnsafeBufferPointer {
            Darwin.write(fd, $0.baseAddress, $0.count)
        }
        // ...
    }
  
    consuming func close() {
        Darwin.close(fd)
    }
  
    deinit {
        Darwin.close(fd)
        // the close operation marked as consuming
        // giving up ownership of copyable == no longer use value
    }
}

요런 코드를 보시면 내부적으로 fd를 참조로 가지고 있어 병목 현상이 일어날 수 있습니다.

그렇기에 꼭 deinit을 시켜줘야 합니다.

close 앞에 consuming이 붙었는데 이를 통해 close가 된 후 write를 발생시키면 컴파일 에러가 나니 실제로 컴파일 타임에서 없는 값을 가져가려는 에러를 사전에 막을 수 있습니다.

 

C++과의 상호 운용성

이제는 C++ 코드를 Swift에서 사용할 수 있고 읽을 수 있도록 컴파일러가 지원됩니다.

// Person.h
struct Person {
    Person(const Person &);
    Person(Person &&);
    Person &operator=(const Person &);
    Person &operator=(Person &&);
    ~Person();
  
    std::string name;
    unsigned getAge() const;
};
std::vector<Person> everyone();

// Client.swift
func greetAdults() {
    for person in everyone().filter { $0.getAge() >= 18 } {
        print("Hello, \(person.name)!")
    }
}

이와 더불어 소노마에서는 Swift로 코드들이 바뀐 부분이 많아 성능이 많이 향상되었다고 합니다.

Calendar를 사용할 경우 20% 향상, Date formatting은 150%, JSON coding은 200~500%까지 성능 향상을 볼 수 있습니다.

Object-C와도 호환성이 많이 개선되었습니다🙌

 

그 다음으로 Actor와 Concurrency 그리고 FoundationDB 부분에서도 많은 소개가 있었는데요.
요 부분들은 덩어리가 커서 후속 포스팅으로 다뤄보겠습니다!

 

마무리

Swift 6이 벌써부터 어떻게 바뀔지 5.9를 보면서 조금 기대되네요 😃

 

참고 자료

https://developer.apple.com/wwdc23/10164

 

What’s new in Swift - WWDC23 - Videos - Apple Developer

Join us for an update on Swift. We'll show you how APIs are becoming more extensible and expressive with features like parameter packs...

developer.apple.com

https://www.hackingwithswift.com/articles/258/whats-new-in-swift-5-9