본문 링크 (Original Link)

스위프트 4.2에선 어떤 것이 새로워졌을까요?

2018.05.20

#

by Paul Hudson, translated by pilgwon

image1

스위프트 4의 두 번째 마이너 릴리즈이며 엄청난 개선점들이 포함돼있는 버전인 스위프트 4.2가 발표되었습니다. 덕분에 올해는 스위프트에게 믿을 수 없을 정도로 좋을 한 해가 될 것이고 커뮤니티가 주도하는 스위프트 혁신 과정은 스위프트 자체를 더 훌륭한 언어로 만들고 있습니다.

오늘은 Enum의 case로 구성된 컬렉션, 경고 및 에러 지시사항 컴파일러에게 넘기기, 동적 멤버 검색 등에 대해 알아볼 것입니다. 모두 제가 스위프트 4.1에 대해 소개할 때도 새로운 기능으로 알려드렸던 것들입니다. 스위프트 4.1에 대해 궁금하시다면 제 글인 스위프트 4.1에선 어떤 것이 새로워졌을까요? 에서 확인하실 수 있습니다. 스위프트 4.2는 마스터 브랜치에 머지된지 며칠도 채 되지 않았습니다. 추가적인 변화가 생긴다면, 이 예제는 업데이트될 것입니다.

Xcode에서 스위프트 4.2를 지원하지 않는다면, 최신 스위프트 개발 스냅샷을 다운로드 받으시고 현재 Xcode 버전에서 활성화하시면 됩니다.

Enum의 case로 구성된 컬렉션

SE-0194 에서는 새로운 CaseIterable 프로토콜이 소개되었습니다. 이 프로토콜은 enum의 모든 case를 배열 속성으로 자동 생성시켜줍니다.

스위프트 4.2 이전에는 이와 같은 결과를 얻으려면 꼼수나 직접 코딩 또는 Sourcery 코드 생성기를 사용했어야 했습니다. 하지만 이제 CaseIterable 에 맞는 enum만 만들면 모든 것이 해결됩니다. 컴파일 할 때, 스위프트는 enum의 모든 case를 배열로 표현해주는 allCases 속성을 여러분이 정한 순서로 자동으로 생성합니다.

예를 들어, 다음은 파스타 모양에 대한 enum을 만들고 스위프트에게 allCases 배열을 자동으로 만들어달라고 하는 코드입니다:

enum Pasta: CaseIterable {
    case cannelloni, fusilli, linguine, tagliatelle
}

이 결과로 나온 속성은 보통의 배열처럼 사용할 수 있습니다. 코드상에선 [Pasta] 의 형태로 주어질 것이고 예시를 보여드리자면 다음과 같습니다:

for shape in Pasta.allCases {
    print("I like eating \(shape).")
}

allCases의 자동 생성은 오로지 enum만을 값으로 가지며, 다른 연관값은 포함되지 않습니다. 그것들을 자동으로 포함한다는 것은 말이 되지 않지만, 필요하다면 직접 추가할 수 있습니다:

enum Car: CaseIterable {
    static var allCases: [Car] {
        return [.ford, .toyota, .jaguar, .bmw, .porsche(convertible: false), .porsche(convertible: true)]
    }

    case ford, toyota, jaguar, bmw
    case porsche(convertible: Bool)
}

추가적으로, 스위프트는 어떠한 enum의 case라도 사용이 불가능하다(unavailable)고 표시되어 있으면 allCases를 만들지 않습니다. allCases를 자동으로 생성하기 위해선 다음과 같이 직접 처리를 해주어야 합니다:

enum Direction: CaseIterable {
    static var allCases: [Direction] {
        return [.north, .south, .east, .west]
    }

    case north, south, east, west

    @available(*, unavailable)
    case all
}

중요: allCases 배열이 자동으로 만들어지기 위해서는 익스텐션이 아닌 원본 정의에서 CaseIterable을 추가해주어야 합니다. 이것은 여러분이 이미 존재하는 enum을 프로토콜에 맞추기 위해서 소급적으로 익스텐션으로 만들어서 적용하는 것이 불가능하다는 것을 의미합니다.

경고 및 에러 지시사항 컴파일러에게 넘기기

SE-0196에서는 코드에 이슈를 마크해주는 새로운 컴파일러 지시가 소개되었습니다. 이는 전에 Objective-C를 사용해본 적이 있으시다면 친근할 것이고, 스위프트 4.2에서는 그 기능을 스위프트로서 즐길 수 있게 되었습니다.

새로운 두 가지 지시는 #warning#error입니다. 전자는 Xcode가 코드를 빌드할 때 경고를 보내도록 하고, 후자는 에러를 발생시켜서 빌드가 되지 않습니다. 둘 다 다른 용도로 매우 유용합니다.

둘 다 사용법은 동일합니다. #warning("Some message")#error("Some message")로 사용하면 됩니다.

func encrypt(_ string: String, with password: String) -> String {
    #warning("This is terrible method of encryption")
    return password + String(string.reversed()) + password
}

struct Configuration {
    var apiKey: String {
        #error("Please enter your API key below then delete this line.")
        return "Enter your key here"
    }
}    

#warning#error 둘 다 #if 컴파일러 지시와 같이도 작동하기 때문에, if의 조건이 맞는 경우에만 작동되도록 할 수 있습니다. 예를 들면 다음과 같습니다.

#if os(macOS)
#error("MyLibrary is not supported on macOS.")
#endif

동적 멤버 검색

SE-0195 에서는 스위프트를 파이썬과 같은 스크립트 언어에 가까워지도록 하면서 타입 안정성을 지키는 방법에 대해 소개합니다. 여러분은 스위프트의 안정성을 잃지 않는 동시에 PHP나 파이썬에서 볼 수 있는 코드를 작성할 수 있는 능력을 가지게 됩니다.

이 기능의 핵심에는 @dynamicMemberLookup라는 새로운 속성이 있습니다. 이 속성은 스위프트가 subscript 메소드를 호출할 수 있게 지시합니다. 이 subscript 메소드(subscript(dynamicMember:))를 사용하려면 스트링 값이 넘어와야 하고 그래야 여러분이 원하는 값을 반환해줄 수 있습니다.

간단한 예제로 이해해봅시다. Person 구조체를 만들고 딕셔너리에서 그 값을 읽는 코드는 다음과 같습니다.

@dynamicMemberLookup
struct Person {
    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Taylor Swift", "city": "Nashville"]
        return properties[member, default: ""]
    }
}

@dynamicMemberLookup 속성은 실제 동적 멤버 검색이 잘 작동하기 위해서 subscript(dynamicMember:) 메소드를 구현해야 합니다. 보시다시피, 저는 멤버의 이름을 스트링으로 받는 코드를 작성했고 내부적으로 멤버의 이름을 둘러본 후에 그 값을 반환합니다.

구조체가 제공하는 메소드 사용 방식은 다음과 같습니다:

let person = Person()
print(person.name)
print(person.city)
print(person.favoriteIceCream)

위의 코드는 name, city, favoriteIceCream 속성이 Person 타입에 존재하지 않더라도 멀쩡하게 컴파일과 실행이 될 것입니다. 대신에, 실행중에 들키게 될 것입니다. 이 코드는 첫 두개의 Print()에서는 “Taylor Swift”와 “Nashville”을 출력할 것이고, 마지막은 빈 스트링을 출력할 것입니다. 왜냐하면 우리는 favoriteIceCream에 대한 값을 입력한 적이 없기 때문입니다.

우리의 subscript(dynamicMember:) 메소드는 반드시 스위프트의 안정성의 기반인 스트링을 반환합니다. 심지어 동적 데이터를 다룰 때에도 스위프트는 여러분이 예상한대로 작동하는 것을 보장할 것입니다. 만약 여러가지 다른 종류의 타입을 원하신다면, 다른 subscript(dynamicMember:) 메소드를 구현하면 됩니다. 다음과 같이요.

@dynamicMemberLookup
struct Employee {
    subscript(dynamicMember member: String) -> String {
        let properties = ["name": "Taylor Swift", "city": "Nashville"]
        return properties[member, default: ""]
    }

    subscript(dynamicMember member: String) -> Int {
        let properties = ["age": 26, "height": 178]
        return properties[member, default: 0]
    }
}

어떤 속성도 한 가지 이상의 방법으로 접근될 수 있게 되었으니, 이제 스위프트는 여러분에게 그 중에 어떤 것을 실행할지 명확하게 하라고 요구할 것입니다. 그럴때에는 예를 들자면 함수의 반환 값을 오로지 스트링으로 받는 것과 같이 만들면 됩니다. 이렇게요.

let employee = Employee()
let age: Int = employee.age

어느 쪽이든, 스위프트는 어떤 subscript가 호출되는지 당연히 알 것입니다.

추가적으로 반환 클로저에 subscript를 추가할 수 있습니다.

@dynamicMemberLookup
struct User {
    subscript(dynamicMember member: String) -> (_ input: String) -> Void {
        return {
            print("Hello! I live at the address \($0).")
        }
    }
}

let user = User()
user.printAddress("555 Taylor Swift Avenue")

위의 예제가 실행되면, user.printAddress는 스트링을 출력하는 클로저를 반환할 것이고, ("555 Taylor Swift Avenue")부분은 바로 그 클로저의 입력으로 들어갈 것입니다.

보통의 속성이나 메소드를 가진 타입에서 동적 멤버를 subscript 하려면, 그 속성과 메소드가 항상 동적 멤버에서 사용되어야 합니다. 예를 들어, 내장된 name 속성을 가진 Singer 구조체를 정의해서 동적 멤버로 subscript한다고 해보겠습니다.

struct Singer {
    public var name = "Justin Bieber"

    subscript(dynamicMember member: String) -> String {
        return "Taylor Swift"
    }
}

let singer = Singer()
print(singer.name)

위 코드는 “Justin Bieber”를 출력할 것입니다. 왜냐하면 name 속성이 동적 멤버 subscript 대신에 사용되었기 때문입니다.

@dynamicMemberLookup는 스위프트의 타입 시스템의 모든 역할을 연기합니다. 프로토콜, 구조체, Enum, 그리고 클래스 모두를 할당할 수 있다는 의미입니다. 심지어 @objc가 표시된 클래스들도요!

실제로, 이것은 두 가지를 의미합니다. 첫 번째는 여러분이 @dynamicMemberLookup를 사용해서 클래스를 만들 수 있고 그것을 상속받은 클래스들 또한 자동으로 @dynamicMemberLookup의 영향을 받는다는 것을 의미합니다. 그러니 다음의 예제는 “I’m a sandwich” 라고 출력할 것이고 HotDogSandwich를 상속받았기 때문이죠.

@dynamicMemberLookup
class Sandwich {
    subscript(dynamicMember member: String) -> String {
        return "I'm a sandwich!"
    }
}

class HotDog: Sandwich { }

let chiliDog = HotDog()
print(chiliDog.description)

메모: 만약 핫도그가 샌드위치에 속하지 않는다고 생각하신다면, 제 트위터를 팔로우하시고 제가 얼마나 잘못됐는지 알려주시겠어요?

두번째로는 프로토콜에서 타입을 소급적으로 @dynamicMemberLookup를 사용하게 할 수 있습니다. 프로토콜 익스텐션에서 subscript(dynamicMember:)의 기본적인 구현방식을 추가하기만 하면 됩니다. 그러면 다른 타입들을 원하는대로 여러분의 프로토콜에 적용되도록 할 수 있습니다.

예를 들어, 다음 예제는 새로운 Subscripting 프로토콜을 만들고, 메세지를 반환하는 기본적인 subscript(dynamicMember:) 구현을 제공하며 스위프트의 String을 그 프로토콜을 사용하도록 만들 것입니다.

@dynamicMemberLookup
protocol Subscripting { }

extension Subscripting {
    subscript(dynamicMember member: String) -> String {
        return "This is coming from the subscript"
    }
}

extension String: Subscripting { }
let str = "Hello, Swift"
print(str.username)

Chris Lattner의 스위프트 혁신 제안서에서 그는 동적 멤버 검색을 더 자연스러운 문법을 만들기 위해 사용하는 JSON enum의 예제를 제공합니다.

@dynamicMemberLookup
enum JSON {
   case intValue(Int)
   case stringValue(String)
   case arrayValue(Array<JSON>)
   case dictionaryValue(Dictionary<String, JSON>)

   var stringValue: String? {
      if case .stringValue(let str) = self {
         return str
      }
      return nil
   }

   subscript(index: Int) -> JSON? {
      if case .arrayValue(let arr) = self {
         return index < arr.count ? arr[index] : nil
      }
      return nil
   }

   subscript(key: String) -> JSON? {
      if case .dictionaryValue(let dict) = self {
         return dict[key]
      }
      return nil
   }

   subscript(dynamicMember member: String) -> JSON? {
      if case .dictionaryValue(let dict) = self {
         return dict[member]
      }
      return nil
   }
}

동적 멤버 검색이 없다면 JSON enum의 객체를 찾기 위해서 다음과 같이 코드를 작성해야 할 것입니다.

let json = JSON.stringValue("Example")
json[0]?["name"]?["first"]?.stringValue

하지만 동적 멤버 검색이 있다면 다음과 같이 대체하면 됩니다.

json[0]?.name?.first?.stringValue

저는 @dynamicMemberLookup가 무엇인지 제대로 보여주는 예제라고 생각하기 때문에 특별히 중요하다고 생각합니다. 이는 커스텀 subscript를 간단한 dot syntax으로 바꿔주기 때문에 문법적인 설탕같은 존재라고 생각하기 때문입니다.

메모: 동적 멤버 검색을 사용하면 완료할 것이 없기 때문에 코드 완전성은 유용성 전부는 아니더라도 많은 것을 잃어버립니다. 하지만 이건 그렇게 놀라운 사실은 아닙니다. 그리고 이것은 파이썬 IDE들도 가끔씩 다루는 부분이기도 합니다. SE-0195의 작성자인 Chris Lattner는 그 제안속에서 코드 완전성의 미래 가능성에 대해 토론했습니다. 매우 읽을만한 가치가 있습니다.

향상된 조건부 적응

조건부 적응은 스위프트 4.1에서 소개된 기능인데, 타입들을 어떠한 조건이 맞을때에만 프로토콜에 적응하도록 해줍니다.

예를 들어, Purchasable 프로토콜을 만들었다고 해보겠습니다.

protocol Purchasable {
    func buy()
}

그리고 이 프로토콜을 따르는 간단한 타입을 만들어보겠습니다.

struct Book: Purchasable {
    func buy() {
        print("You bought a book")
    }
}

게다가 모든 요소가 Purchasable 을 따른다면 Purchasable 을 따르는 Array 도 만들 수 있을 것입니다.

extension Array: Purchasable where Element: Purchasable {
    func buy() {
        for item in self {
            item.buy()
        }
    }
}

이 코드는 컴파일할 때는 아주 잘 작동하지만, 문제가 하나 있습니다. 만약 여러분이 조건부 적응을 런타임에서 필요로 한다면, 여러분의 코드는 에러를 낼 것입니다. 왜냐하면 스위프트 4.1에서는 지원하지 않기 때문이죠.

그렇지만 스위프트 4.2에서는 해결되었으니 만약 여러분이 하나의 타입을 받았고 그것이 조건부 적응 프로토콜로 전환될 수 있는지 확인할 수 있습니다. 아주 잘 작동합니다.

예를 들자면 다음과 같습니다.

let items: Any = [Book(), Book(), Book()]

if let books = items as? Purchasable {
    books.buy()
}

게다가, Hashable 적응에 대한 자동 문법 지원이 스위프트 4.2에서 더욱 더 향상되었습니다. 옵셔널, 배열, 딕셔너리 그리고 range 등 스위프트 표준 라이브러리의 몇몇 내장된 타입은 이제 그들의 요소가 Hashable에 적응한다면 자동으로 Hashable 프로토콜에 적응하게 됩니다.

예를 들어 보겠습니다.

struct User: Hashable {
    var name: String
    var pets: [String]
}

스위프트 4.2에서는 위의 구조체에 자동으로 Hashable에 적응하지만 스위프트 4.1은 그러지 못합니다.

제자리에서 컬렉션 요소 삭제하기

SE-0197 에서는 새로운 removeAll(where:) 메소드가 소개되었고 이는 컬렉션을 제자리에서 필터링해주는 고성능 메소드입니다. 클로저가 실행될 조건을 주면, 그 메소드는 그 조건에 맞는 객체들을 모두 내보냅니다.

예를 들어, 만약 여러분이 이름의 컬렉션을 가지고 있고 “Terry”라고 불리는 사람을 삭제하고 싶다면, 다음과 같이 하시면 됩니다.

var pythons = ["John", "Michael", "Graham", "Terry", "Eric", "Terry"]
pythons.removeAll { $0.hasPrefix("Terry") }
print(pythons)

위의 예제를 보고 다음과 같이 filter()를 사용하는 것과 어떤 다름이 있는지 궁금하신 분이 있을 것입니다.

pythons = pythons.filter { !$0.hasPrefix("Terry") }

하지만 거기엔 메모리 사용에 있어서 큰 차이가 있습니다. 둘의 차이는 여러분이 원하는 것을 걸러내는지 원하지 않는 것을 걸러내는지이고, 더 발전된 제자리 해결책은 초보자들이 불편을 겪을 수 있는 복잡성의 한계선 안에서 해결됩니다. SE-0197의 작성자인 Ben Cohen이 dotSwift 2018에서 이 제안서의 구현 방식에 대해 토론했던 대화를 공유드립니다. 만약 여러분이 이것이 왜 효율적인지를 더 자세히 알고싶으시다면, 여기서 확인하시면 될 것입니다!

불리언 토글링

SE-0199 에서는 불리언의 값을 참, 거짓으로 왔다갔다 할 수 있는 새로운 toggle() 메소드가 소개되었습니다. 이 메소드때문에 스위프트 커뮤니티에서 많은 토론이 있었고, 일부는 너무 사소하기 때문에 넣을 이유가 없다고 했고, 어떨때는 스위프트 포럼의 통제를 벗어나기도 했습니다.

이 제안의 전체 스위프트 코드는 몇 줄이 되지 않습니다.

extension Bool {
   mutating func toggle() {
      self = !self
   }
}

하지만 결과는 스위프트 코드를 더욱 더 자연스럽게 만들어주었습니다.

var loggedIn = false
loggedIn.toggle()

제안서에도 적혀있듯이, 이것은 데이터 구조가 더 복잡해질수록 더욱 더 사용성이 높아질 것입니다. myVar.prop1.prop2.enabled.toggle() 에선 수동으로 값을 바꿔서 발생할 수 있는 입력 오류를 피할 수 있습니다.

이 제안은 스위프트 코드 작성을 쉽고 안전하게 만들어주었습니다. 그리고 순전히 추가하는 것이기 때문에, 저는 대부분의 사람들이 이것을 빠르게 사용할 것이라고 생각합니다.

스위프트 5.0을 앞두며

애플은 스위프트 4.2를 “스위프트 5에서의 ABI 안정성을 이루기 위한 중간 지점”이라고 설명했습니다. 하지만 제 생각엔 이건 조금 과소 평가된 거라고 생각합니다. 우리는 앞에서 ABI가 변한 것에 더하여 지금까지 훌륭한 새로운 기능들과 이전 기능이 아주 잘 개선된 것을 보았기 때문입니다.

더 말하자면, 최종 버전 이전에 추가 기능이 생긴 것을 볼 수 있습니다. 그 목록은 SE-0192, SE-0193, SE-0202, 그리고 SE-0206가 있는데, 우리가 봤던 것들이거나 동시에 나온 것입니다.

많은 사람들이 ABI 안정성을 높인 스위프트 5.0을 기다리고 있습니다. 이번 애플의 조심스러운 접근 방식은 효과가 있을 것이라고 생각하고, 또한 스위프트 5.0이 기다리고 있던 것보다 더욱 더 좋을 것이라는 것을 의미한다고 희망적으로 생각합니다.