Swift 5가 지원되는 Xcode 10.2가 정식으로 릴리즈 되었습니다. 이에 발맞춰 (그리고 공부 삼아) Swift 5의 변경점을 정리해 보는 글입니다.
이 글은 공식 블로그의 Swift 5 릴리즈 소식을 기준으로 작성합니다.
ABI Stability
Swift 5는 ABI Stability, 즉 바이너리 레벨의 API 호환성을 드디어 안정화시킨 버전입니다. 이제 사용하는 프레임워크나 라이브러리가 Swift 5 이상으로 빌드했다면 컴파일러가 버전 다르다고 투덜거리는 것을 볼 확률이 많이 없어진다는 말입니다.
Raw Text
SE-0200 Enhancing String Literals Delimiters to Support Raw Text
이스케이프(escape) 등 문자열 대치(String Interpolation)이 되지 않는 순수한 문자열을 정의하는 기능이 추가되었습니다. 샾(#) 문자를 섞어서 #"
... "#
로 표기된 문자열은 이스케이프가 되지 않습니다.
var message = "Insert this code: \(1 + 2)"
위 라인은 "Insert this code: 3"
라는 문자열을 만들어냅니다.
하지만 만약 원하는 것이 만약 위 문자열이 그대로 쓰인 데로라면 아래처럼 코딩해야 합니다.
var message = "Insert this code: \\(1 + 2)"
이미 잘 알려진 백슬래시 앞에 백슬래시를 하나 더 붙이는 방법이지요.
그런데 만약 이런 식으로 백슬래시를 붙여야 할 내용이 많아진다면 굉장히 귀찮을 것입니다. 그래서 이번 기능이 추가되었습니다.
var rawMessage = #"Insert this code: \(1 + 2)"#
위 코드에서 rawMessage
문자열은 "Insert this code: \(1 + 2)"
로 정의가됩니다. 쓰인 그대로 만들어지는 것이지요. 즉 #"
로 시작해서 "#
로 끝나는 방법으로 문자열을 정의하면 내부의 이스케이프 등등이 몽땅 무시됩니다. 마치 Python에서 r"..."
로 표기하는 순수문자열(?) 표기 방식과 동일합니다.
만약 문자열 데이터에 겹따옴표를 많이 써야 한다면 #""" ... """#
문법을 쓰는게 훨씬 편하겠지요?
compactMapValues
SE-0218 Introduce compactMapValues to Dictionary
사전형(Dictionary)에 compactMapValues
기능이 추가되었습니다. 참고로 compactMap
은 대충(?) 기존 flatMap
이 하던 nil
을 걸러내는 기능만 대체하는 메서드입니다. 그리고 map
에 대응하는 compactMap
이 있다면 mapValues
에 대응하는 compactMapValues
기능도 있어야 마땅하겠지요. 이번에 추가된 것이 바로 이것입니다.
참고로 mapValues
및 compactMapValues
메소드는 사전형(Dictionary)에서 값(value)을 변경하는 용도로 사용됩니다.
let dict: [String : Int?] = ["a": 1, "b": 2, "c": nil, "d": 4]
print(dict.compactMapValues { $0 ?? nil })
// ["a": 1, "b": 2, "d": 4]
compactMapValues
를 통해 호출되는 클로져의 입력이 옵셔널이기 때문에 필요 없는 값은 nil
을 리턴하면 filter가 되는 효과가 있다는 점이 특징적입니다.
Result
SE-0235 Add Result to the Standard Library
이제는 유명해져버린 Result
타입이 이제 정식으로 Swift 5의 일원이 되었습니다.
혹시나 모르는 분을 위해서 잠깐 소개하자면, Result
타입은 성공과 성공시의 데이터, 그리고 실패와 실패 시 에러를 묶어서 구조화시킨 타입입니다. 예를 들어 아래의 예를 봅시다.
if let result = someWork() {
doWhat(with: result)
}
위 코드에서 만약 someWork()
가 nil
을 리턴하는 것이 에러인지 아닌지 어떻게 알 수 있을까요? 보통은 nil
을 에러처럼 생각할 수도 있겠지만 정해진 활용법이 아닙니다. 해당 함수나 메서드에 대해 명시적으로 어떤 값의 의미가 무엇인지 확실히 매뉴얼화해놓지 않았다면 모호함이 남게 됩니다.
대신 Result
타입을 도입하면 달라질 수 있습니다. 위의 someWork()
라는 함수가 만약 Result
타입 형식으로 리턴한다면 아래와 같이 코드가 변화합니다.
switch someWork() {
case .success(let result):
doWhat(with: result)
case .failure(let error):
log(error)
}
Result
타입을 사용하면 성공(.success
)과 실패(.failure
)라는 두 가지 결과를 명확하게 확인할 수 있고, 성공 시의 데이터(result
)와 에러(error
)를 명확하게 구분할 수 있게 되는 두 가지 이점을 가지게 됩니다.
다만 아직 iOS/macOS용 프레임워크 자체는 이에 대응하지는 않은 것 같긴 합니다.
Key Path
특정 오브젝트의 키 패스 표현 문법에서 자기 자신을 표현하는 문법이 추가되었습니다.
struct Person {
var name = "default name"
var age = 16
}
var kim = Person()
print(kim.self) // Swift 4.2 이상
print(kim[keyPath: \.self]) // Swift 5 이상
// Person(name: "default name", age: 16)
기존 4.2 버전에서는 object.self
와 같은 방식으로 표현했지만 버전 5부터는 이제는 \.self
로 표현이 가능해집니다.
Dynamically Callable
SE-0216 Introduce user-defined dynamically “callable” types
이미 Swift 4.2에서 추가된 @dynamicCallable
에 새로운 새로운 문법이 추가되었습니다. 아니 변경되었다고 해야 되려나요?
일단 기존 예제를 봅시다.
struct NalzzaGenerator {
func generate(year: Int, month: Int, day: Int) -> Date? {
var dc = DateComponents()
dc.year = year
dc.month = month
dc.day = day
return Calendar.current.date(from: dc)
}
}
let n = NalzzaGenerator()
n.generate(year: 2019, month: 4, day: 1)
위 코드는 살짝 바보 같은 디자인이지만 일부러 이렇게 만들었다고 가정합니다. 뭐 하여간 년/월/일을 입력받아 Date
타입의 값을 생성하는 generate()
메서드를 가지는 Nalzza(날짜ㅋ) 제너레이터 구조체입니다.
이제 본론으로 들어가서, Swift 5부터는 @dynamicCallable
형태로 아래와 같은 형식으로 코드를 작성할 수도 있습니다.
@dynamicCallable
struct NalzzaGenerator {
func dynamicallyCall(withKeywordArguments args: KeyValuePairs<String, Int>) -> Date? {
var dc = DateComponents()
for (key, value) in args {
if key == "year" {
dc.year = value
} else if key == "month" {
dc.month = value
} else if key == "day" {
dc.day = value
}
}
return Calendar.current.date(from: dc)
}
}
let nalzzaGenerator = NalzzaGenerator()
nalzzaGenerator(year: 2019, month: 4, day: 1)
key-value 체크를 조금 철저히 했더니 약간 복잡해졌지만 앞서 본 generator()
메소드와 동일한 동작의 코드입니다. 예제는 struct
로 만들기는 했지만 class
로도 구현할 수 있습니다.
핵심은 이 NalzzaGenerator
타입의 인스턴스를 마치 함수처럼 접근한다는 점입니다. 그리고 이렇게 액세스 할 때 호출되는 메서드가 바로 이번에 추가된 dynamicallyCall()
입니다.
dynamicallyCall
에는 withArguments라는 인터페이스가 하나 더 있습니다. 이 인터페이스는 필드명을 생략하고 데이터만으로 호출할 경우 동작합니다. C 함수처럼 필드 이름을 생략하는 형식으로 쓰기 위해서 아래와 같은 식으로 고칠 수 있습니다.
@dynamicCallable
struct NalzzaGenerator {
func dynamicallyCall(withArguments args: [Int]) -> Date? {
var dc = DateComponents()
dc.year = args[0]
dc.month = args[1]
dc.day = args[2]
return Calendar.current.date(from: dc)
}
}
let nalzzaGenerator = NalzzaGenerator()
nalzzaGenerator(2019, 4, 1)
개인적으로는 필드 이름이 있는 형태를 선호합니다만 쓰는 사람 마음이지요.
@dynamicCallable
은 이렇게 특수한 dynamic member lookup을 구현할 때 필요하며, 이 지시어가 명시된 경우 멤버는 반드시 dynamicallyCall로만 구성해야 하며 withKeywordArguments: 혹은 withArguments: 둘 중 하나만 구현되어도 됩니다.
컴파일러 관련
SE-0224 Support ‘less than’ operator in compilation conditions
에 그러니까... 위 내용을 간단히 정리하자면 이제 아래와 같은 코드가 제대로 컴파일된다는 의미입니다.
#if swift(<5)
...
#endif
#if swift(<=4.2)
...
#endif
이게 지금껏 안 되었다는 것이 신기할 정도네요.
기타
버그 픽스나 내부 구조 개선 등을 제외하면 아래와 같은 변화점을 찾을 수 있습니다.
- SE-0192 Handling Future Enum Cases: switch 문에
@unknown default
를 쓸 수 있게 되었는데, Objective-C로 쓰인 NS_ENUM 열거형 타입의 케이스가 나중에 늘어날 가능성이 있을 경우 쓸 수 있습니다. - SE-0211 Add Unicode Properties to Unicode.Scalar: Unicode.Scalar에 몇 가지 프로퍼티가 추가되었습니다.
- SE-0214 Renaming the DictionaryLiteral type to KeyValuePairs: 이름의 모호성 때문에 DictionaryLiteral 타입의 이름이 KeyValuePairs로 바뀌었습니다. ABI Stability와 관계가 있기 때문에 일반적인 앱 개발자에겐 그다지 흥미 있는 주제는 아닐 것 같습니다.
- SE-0215 Conform Never to Equatable and Hashable: Never라는 타입이 Equatable과 Hashable 프로토콜을 만족합니다. 참고로 Never는 이름처럼 '오류' 그 자체를 나타내는 타입입니다. 예를 들자면 fatalError()를 리턴하는 경우 이 Never타입을 리턴 타입으로 쓸 수 있습니다.
- SE-0221 Character Properties: Character 타입에 문자 정보와 관련된 여러 프로퍼티가 추가되었습니다.
- SE-0225 Adding isMultiple to BinaryInteger: 정수 데이터의 2진 데이터 표기법을 정의한 BinaryInteger 프로토콜에 배수(예를 들어 2의 배수, 3의 배수 등)를 확인할 수 있는 메서드인 isMultiple이 추가되었습니다. 사실상 거의 모든 정수형 타입에 isMultiple이 추가되었다는 이야기와 동급입니다.
- SE-0229 SIMD Vectors: CPU SIMD("Single Instruction, Multiple Data") Instruction를 사용하기 위한 것인데 관련 지식이 없어서 자세히는 모르겠네요.
- SE-0232 Remove Some Customization Points from the Standard Library’s Collection Hierarchy: 이건 뭘까요? 음?
- SE-0233 Make Numeric Refine a new AdditiveArithmetic Protocol: Numeric 프로토콜 개선과 관련된 사항?
- SE-0234 Remove Sequence.SubSequence: 콜렉션 타입의 아이템 배치를 수정해서 일부만 돌려주는 경우 사용하던 SubSequence 타입이 삭제됩니다. 이제 명확한 Swift 식 컬렉션 표기를 사용해야 합니다.
- SE-0237 Introduce withContiguous{Mutable}StorageIfAvailable methods: 일부 콜렉션 타입에 몇 가지 메서드가 추가됩니다. 타입마다 이름이 다를 수 있습니다.
- SE-0239 Add Codable conformance to Range types: 범위(Range) 타입이 이제 Codable을 만족합니다. 예를 들어 Codable 형식의 타입을 만들 때 Range 타입도 마음껏 슬 수 있으며 따라서 JSONEncoder, JSONDecoder를 이용하는 것이 약간 수월해집니다.
- SE-0241 Deprecate String Index Encoded Offsets: 이제부터 문자열 타입에서 encodedOffset을 사용할 수 없습니다. 대안으로 utf16Offset을 쓸 수 있습니다.
마무리
이전 버전들의 변화에 비해 Swift 4에서 5로 넘어가는 길은 생각보다 순탄한 것 같습니다. 사라진 특수한 문법이나 표준 라이브러리의 변화가 거의 없는 편이지요. 따라서 새로 생긴 것들이 무엇인지 대충 파악만 해둬도 별로 문제는 없을 것 같습니다.
아래는 이 글과 관련이 있는 링크입니다:
- Swift 4 - Codable / JSONDecoder / JSONEncoder - 옛날 블로그(SEORENN SIGSEGV)
- 눈에 띄는 Swift 4 변경점들 (Xcode 9 첫 Beta 기준) - 옛날 블로그(SEORENN SIGSEGV)
'기술적인 이야기 > 애플 플랫폼 개발' 카테고리의 다른 글
[Swift 5.1] 초기화 없는 배열 생성하기 (SE-0245) (0) | 2019.10.24 |
---|---|
[Swift 5.1] Opaque Result Types 그리고 some (SE-0244) (0) | 2019.10.18 |
[Swift 5.1] 구조체 생성자에 기본값 지원 (SE-0242) (0) | 2019.10.17 |
[Swift 5.1] 순서가 있는 콜렉션끼리 차이점 비교하기 (SE-0240) (0) | 2019.10.15 |
[Swift 5.1] Self 키워드의 기능 추가 (SE-0068) (1) | 2019.10.13 |
댓글