제목: Ultimate Guide to JSON Parsing With Swift 4

스위프트4와 Foundation은 마침내 스위프트에서 JSON을 파싱하는 질문에대한 대답을 내놓았다.

파싱을 위한 훌륭한 라이브러리들이 많이 있었지만, 이렇게 적용하기 쉬울뿐 아니라 복잡한 시나리오에따라 커스터마이징이 가능한 솔루션은 꽤나 신선하다.

여기서 말하는 모든 것들을 어떤 Encode/Decoder 구현에 적용해보는 것은 의미없는 일이며, PropertyListEncoder도 마찬가지이다. 여러분이 XML같은 다른게 필요하면 커스텀 구현을 만들수도 있다. 이 블로그 포스트에서는 JSON 파싱에대해 초점을 맞추게 될것인데, 이것이 가장 많은 iOS 개발자와 연관되었기 때문이다.

기본
여러분의 JSON 구조와 오브젝트가 비슷한 구조를 가진다면 일은 쉽게 풀린다.

아래에는 맥주에대한 JSON 문서 예시이다.
{
   "name": "Endeavor",
   "abv": 8.9,
   "brewery": "Saint Arnold",
   "style": "ipa"
}
우리의 스위프트 자료구조는 이렇게 생길 수 있다.
enum BeerStyle : String {
   case ipa
   case stout
   case kolsch
   // ...

}


struct Beer {
   let name: String
   let brewery: String
   let style: BeerStyle
}
이 JSON 문자열을 Beer 인스턴스로 변환하기위해서, 우리는 타입에다 Codable을 넣을 것이다.

Codable은 사실 Encodable & Decoable로 구성된 유니온 타입(union type)으로, 만약 한방향으로 변환하는 기능만 원하면 적절한 프로토콜을 적용시키면 된다. 이것이 스위프트4의 새로운 기능이다.

Codable은 디폴트 구현이 따라오는데, 대부분의 경우 여러분은 그냥 이 프로토콜을 적용시키고 공짜로 유용한 디폴트 동작을 만끽하면 된다.
enum BeerStyle : String, Codable {
  // ...

}


struct Beer : Codable {
  // ...

}

다음으로 그냥 디코더를 만들어야한다.
let jsonData = jsonString.data(encoding: .utf8)!


let decoder = JSONDecoder()

let beer = try! decoder.decode(Beer.self, for: jsonData)
이게 다다!  우리의 JSON 문서를 beer 인스턴스에 파싱하였다. 키의 이름과 타입이 서로 일치하기 때문에 다른 커스터마이징이 필요없었다.

여기서 try!를 사용하는 것은 의미가 없겠지만, 여러분의 앱에서는 똑똑하게 에러를 캐치하여 다뤄줘야한다. 나중에 에러 핸들링에대해 더 다뤄볼 것이다...

이제 우리가 인위적으로 만든 예제에서는 완벽하게 정돈되었다. 그러나 만약 타입이 일치하지 않는다면 어떨까?

키 이름을 커스터마이징하기
API에서 키 이름을 snake-case로 하는 경우는 종종 있고, 이 스타일은 스위프트 속성에대한 네이밍 가이드라인과는 맞지 않는다.

이 부분을 커스터마이징하려면 잠시 Codable 디폴트 구현을 맞춰줘야한다.

키는 컴파일러가 자동으로 생성한 "CodingKeys" 열거형에의해 처리된다. 이 열거형은 CodingKey를 따르는데, 이것은 인코딩된 양식으로 속성을 값에 연결할 수 있게 정의한다.

키를 커스터마이징하기위해 우리는 이것에대한 우리만의 구현을 작성할 것이다. 이 경우, 스위프트 네이밍과는 다르며, 우리는 키에대한 문자열 값을 제공할 수 있다.
struct Beer : Codable {
     // ...

     enum CodingKeys : String, CodingKey {
         case name
         case abv = "alcohol_by_volume"
         case brewery = "brewery_name"
         case style
   }
}
만약 beer 인스턴스를 받아서 JSON으로 인코딩하려하면, 새롱누 포맷으로 동작하는 것을 볼 수 있다.
let encoder = JSONEncoder()

let data = try! encoder.encode(beer)
print(String(data: data, encoding: .utf8)!)
이것은 아웃풋이다.
{"style":"ipa","name":"Endeavor","alcohol_by_volume":8.8999996185302734,"brewery_name":"Saint Arnold"}
여기서 표현된 양식은 매우 인간-친화적이지않다. 더 보기좋게 만들기위해 outputFormatting 프로퍼티로 JSONEncoder 아웃풋 포맷을 커스터마이징 할 수 있다.

디폴트값은 .compact인데, 이것은 위의 결과처럼 만들어준다. 이것을 .prettyPintted로 바꿔서 더 가독성좋은 아웃풋으로 만들 수 있다.
encoder.outputFormatting = .prettyPrinted

{
  "style" : "ipa",
  "name" : "Endeavor",
  "alcohol_by_volume" : 8.8999996185302734,
  "brewery_name" : "Saint Arnold"
}
JSONEncoderJSONDecoder 둘 다 그 동작을 커스터마이징하기위한 더 많은 옵션들이 있다. 더 일반적으로 해야하는 것중 하나는 날짜 파싱을 어떻게 커스터마이징할것인지이다.

날짜 다루기
JSON은 날짜를 표현하는데 데이터 타입이 없으므로 클라이언트와 서버가 동의한 방법대로 표현하여 시리얼라이즈하게된다. 일반적으로 ISO 8601 날짜 포멧팅 방식으로 처리하며 문자열로 시리얼라이즈한다.
프로 팁: ISO 8601을 포함한 다양한 포멧으로 변환해보려면 nsdateformatter.com에서 해볼 수 있다.
다른 포멧들은 아마 레퍼런스 날짜로부터 샌 초단위(혹은 밀리초단위)로, JSON 문서에서 Number로 시리얼라이즈 될 수 있다.

예전에는 이것을 우리 스스로 처리하여야 했었는데, 아마 우리 데이터 타입에 문자열 칸을 제공하고, 문자열 값에서 날짜를 marshal하기위해 DateFormatter를 사용한다. 그 반대도 마찬가지이다.

JSONEncoderJSONDecoder로 모두 해결할 수 있다. 한번 확이해보자. 날짜를 다루는 스타일로 디폴트로 .deferToDate를 사용할 것이다. 이것은 아래처럼 생겼다.
struct Foo : Encodable {
    let date: Date
}


let foo = Foo(date: Date())
try! encoder.encode(foo)

{
  "date" : 519751611.12542897

}
이것을 .iso8601 포멧으로 바꿀 수 있다.
encoder.dateEncodingStrategy = .iso8601

{
  "date" : "2017-06-21T15:29:32Z"
}
다른 JSON 인코딩 전략도 가능하다.
  • .formatted(DateFormatter): 지원하고자하는 표준 날짜 포멧 문자열이 없을때. 여러분의 date formatter 인스턴스를 제공한다.
  • .custom( (Date, encoder) throws -> Void ): 아주 커스텀하고 싶을때, 여기 블럭을 전달하여 제공된 인코더로 데이터를 인코딩 할 것이다.
  • .millisecondsSince1970.secondsSince1970: API에서 아주 일반적인 양식은 아니다. 이것은 인코딩된 표현에서 타임 지역 정보를 완전히 손실해버리기 때문에 별로 추천하지 않는다. 그런 이유로 누군가가 잘못된 가정을 생각하기 쉽게 만든다.
날짜를 디코딩하는 것은 필수적으로 같은 옵션을 가지지만, .custom에대해서는 .custom( (Decoder) throws -> Date )의 모양을 받는다. 따라서 우리는 디코더를 받고, 디코더 안에서 된 것이 무엇이든 그것에서 날짜로 만들 책임을 가진다.

Float 다루기
Float은 스위프트의 Float 타입과 JSON과는 꽤 맞지 않는 또다른 부분이다. 만약 서버가 유효하지 않은 "NaN"을 문자열로 보내면 어떻게 될까? 양수나 음수 Infinity는 어떨까? 이것들은 스위프트의 어떤 값에도 매칭되지 않는다.

디폴트로 구현된 것은 .throw이다. 디코더가 이 값들을 만나면 에러가 나타날 것이며, 우리가 처리하고 싶은대로 맵핑할 것을 제공할 수 있다.
{
  "a": "NaN",
  "b": "+Infinity",
  "c": "-Infinity"
}

struct Numbers : Decodable {
  let a: Float
  let b: Float
  let c: Float
}
decoder.nonConformingFloatDecodingStrategy =  .convertFromString(
     positiveInfinity: "+Infinity",
     negativeInfinity: "-Infinity",
     nan: "NaN"
)


let numbers = try! decoder.decode(Numbers.self, from: jsonData)dump(numbers)
이렇게 나타난다.
▿ __lldb_expr_71.Numbers

  -a: inf


  -b: -inf


  -c: nan
JSONEncodernonConformingFloatEncodingStrategy로 반대로도 할 수 있다.

주로 일어나는 경우의 그런것은 아니지만 어느날 유용하게 다가올 것이다.

데이터 다루기
가끔 base64로 인코딩된 문자열의 작은 비트의 데이터를 보내는 API를 마주칠 수도 있겠다.

이것을 자동으로 다루려면 이런 인코딩 전략중 하나를 JSONEncoder에 넣어준다.
  • .base64
  • .custom( (Data, Encoder) throws -> Void)
디코딩하기 위해서 JSONDecoder에 디코딩 전략을 넣어준다.
  • .base64
  • .custom( (Decoder) throws -> Data)
당연하게도 여기서는 .base64가 일반적인 선택일 될 것이지만, 커스텀화된 것이 필요하다면 블럭기반 전략에 사용하면 된다.

랩퍼 키(Wrapper Keys)
종종 API는 랩퍼 키를 포함할 것인데, 최상위 JSON 엔티티는 항상 오브젝트이다.

이런식으로 생겼을 것이다.
{
  "beers": [ {...} ]
}
스위프트에서 표현하기위해 우리는 이 응답에대해 새로운 타입을 만든다.
struct BeerList : Codable {
   let beers: [Beer]
}
실제로 이게 다다! 우리 키 이름이 일치하고 Beer이 이미 Codable이므로 그냥 동작한다.

루트 수준 배열
만약 API가 루트 엘리먼트로 배열을 반환하고 있다면 이런식으로 응답을 파싱한다.
let decoder = JSONDecoder()

let beers = try decoder.decode([Beer].self, from: data)
여기 타입으로 Array를 사용하고 있다는 점을 주목하자. Array<T>T가 디코딩가능할때만 디코딩 가능하다.

랩핑키 오브젝트(Object Wrapping Keys) 다루기
여기에는 여러분이 만날 수 있는 또다른 시나리오가 있다. 배열안에 각 오브젝트가 있는 한 배열 응답이 키로 랩핑되있다.
[
  {
   "beer" : {
     "id": "uuid12459078214",
     "name": "Endeavor",
     "abv": 8.9,
     "brewery": "Saint Arnold",
     "style": "ipa"
   }
  }
]
이 키를 붙잡아두기위해 위의 랩핑 타입 방법을 쓸 수도 있지만, 이 구조는 이미 강타입으로 디코딩가능하게 구현되있음을 인지하는 것이 더 쉬우 방법일 수 있다.

보이는가?
[[String: Beer]]
혹은 이런 경우가 더 읽기 쉬울지도 모르겠다.
Array<Dictionary<String, Beer>>
Array<T>이 디코딩가능한것처럼, KT가 둘 다 디코딩가능하면 Dictionary<K, T>도 그렇다.
let decoder = JSONDecoder()

let beers = try decoder.decode([[String:Beer]].self, from: data)
dump(beers)

▿ 1 element
  ▿ 1 key/value pair
    ▿ (2 elements)
      - key: "beer"
      ▿ value: __lldb_expr_37.Beer
        - name: "Endeavor"
        - brewery: "Saint Arnold"
        - abv: 8.89999962


       - style: __lldb_expr_37.BeerStyle.ipa

더 복잡하게 네스티드된 응답(Nested Response)
때론 우리 API 응답은 간단하지 않다. 아마 제일 상위에는 응답의 오브젝트를 정의하는 키가 단순히 하나가 아니고, 여러 컬랙션을 받거나 페이지로 된 정보를 받을 것이다.

예를 들어보자.
{
   "meta": {
       "page": 1,
       "total_pages": 4,
       "per_page": 10,
       "total_records": 38
   },
   "breweries": [
       {
           "id": 1234,
           "name": "Saint Arnold"
       },
       {
           "id": 52892,
           "name": "Buffalo Bayou"
       }
   ]
}
json을 인코딩/디코딩 할때, 스위프트에서 실제로 타입을 중첩할 수 있고 구조체 표현을 가질 수 있다.
struct PagedBreweries : Codable {
   struct Meta : Codable {
       let page: Int
       let totalPages: Int
       let perPage: Int
       let totalRecords: Int
       enum CodingKeys : String, CodingKey {
           case page
           case totalPages = "total_pages"
           case perPage = "per_page"
           case totalRecords = "total_records"
       }
   }

   struct Brewery : Codable {
       let id: Int
       let name: String
   }

   let meta: Meta
   let breweries: [Brewery]
}
이런 방법의 커다란 이점은 다른 응답을 같은 타입의 오브젝트로 가질 수 있다는 점이다(이 경우 아마 보이는것처럼 응답 리스트에서 "brewery"idname만 가지지만, brewery를 선택하면 더 많은 속성을 가진다.(but has more attributes if you select the brewery by itself)) 여기서 Brewery 타입은 중첩되있으므로 다른 Brewery타입을 만들어서 다른 구조체를 디코딩/인코딩할 수 있다.

더 깊은 커스터마이징
지금까지 무거운 작업도 디폴트 EncodableDecoable 구현에 의존해왔다.

대부분 이렇게 처리하겠지만, 결국 우리는 인코딩과 디코딩을 더 컨트롤하기위해 깊이 들어가야 할것이다.

커스텀 인코딩
시작하면서, 컴파일러가 공짜루 우리에게 제공해줬던 것을 커스텀하는 버전을 만들어 볼 것이다. 인코딩으로 시작해보자.
extension Beer {
  func encode(to encoder: Encoder) throws {

   }
}
그리고 이 예제를 좀 더 다루기위해 beer 타입에 새로운 필드 몇개도 추가하고 싶다.
struct Beer : Coding {
   // ...

   let createdAt: Date
   let bottleSizes: [Float]
   let comments: String?


   enum CodingKeys: String, CodingKey {
       // ...

       case createdAt = "created_at",
       case bottleSizes = "bottle_sizes"
       case comments
   }
}
이 메소드에는 인코더를 넣고, "컨테이너(container)"를 받아서, 여기에 값을 인코딩한다.

컨테이너가 무엇일까?
컨테이너는 몇가지 타입중에 하나가 될 수 있다.
  • Keyed Container: 키로 값을 제공함. 이것은 원래 딕셔너리임.
  • Unkeyed Container: 키 없이 정렬된 값을 제공함. JSONEncoder에서는 배열을 의미함.
  • Single Value Container: 담겨진 엘리먼트의 어떤 종류도 없이 가공되지 않은 값을 만듦.
우리의 모든 프로퍼티를 인코딩하기위해 우리는 먼저 컨테이너를 받아야한다. 이 포스트 처음에 나왔던 JSON 구조체를 보면, keyed container가 필요해보인다.
var container = encoder.container(keyedBy: CodingKeys.self)
여기서 2가지를 짚어보자.
  • 컨테이너는 우리가 변경할 수 있도록 반드시 mutable 프로퍼티여야하는데, 변수를 var로 선언해야한다.
  • 키들을 지정해야하는데(그리하여 프로퍼티와 키를 매핑시킨다) 이 컨테이너로 인코딩시킬 수 있는 키가 무엇인지 안다.
후자는 아주 강력한 점이 될 것이다.

다음으로 컨테이너에 값을 인코딩한다. 이 호출들은 모두 에러를 던지므로, 각 줄마다 try로 시작할 것이다.
try container.encode(name, forKey: .name)
try container.encode(abv, forKey: .abv)
try container.encode(brewery, forKey: .brewery)
try container.encode(style, forKey: .style)
try container.encode(createdAt, forKey: .createdAt)
try container.encode(comments, forKey: .comments)
try container.encode(bottleSizes, forKey: .bottleSizes)
comments 필드에서, Encodable의 디폴트 구현은 옵션 값에 encodeIfPresent를 사용한다. 만약 nil이면 인코딩된 표현에서 키는 잃어버릴 것이다는 의미이다. 이것은 API에대해 일반적으로 좋은 해결책이 아니므로, 여기에 null 겂아 있다해도 키를 가지도록 하는 방법이 좋은 방법이다. 여기서 우리는 encodeIfPresent(_:forKey:) 대신 encode(_:forKey:)를 사용하여 이 키를 포함하는 아웃풋으로 만든다.

bottoleSizes 값은 자동으로 인코딩되있지만, 어떤 이유로 커스터바이징이 필요하다면 우리만의 컨테이너를 만들어야한다. 여기서 우리는 각 항목별로 처리하고(부동소숫점을 반올림하여) 순서대로 컨테이너에 추가한다.
var sizes = container.nestedUnkeyedContainer(
     forKey: .bottleSizes)

try bottleSizes.forEach {
     try sizes.encode($0.rounded())
}
그리고 끝났다! 여기에는 부동소수점이 따르는 전략이나 날짜 포멧팅에대한 얘기는 없다는 점을 인지하자. 사실 이 메소드는 전적으로 JSON agnostic인데, 이것이 설계의 부분이다. 인코딩과 디코딩 타입은 제네릭 기능이고, 포멧은 필요한 사람에의해 쉽게 명세된다.

이제 우리가 인코딩한 JSON은 이렇게 생겼다.
{
  "comments" : null,
  "style" : "ipa",
  "brewery_name" : "Saint Arnold",
  "created_at" : "2016-05-01T12:00:00Z",
  "alcohol_by_volume" : 8.8999996185302734,
  "bottle_sizes" : [
   12,
   16
  ],
  "name" : "Endeavor"
}
여기서 부동소수점 값이 원래 JSON 문서에서는 8.9였는데 메모리에 표현되면서 같지 않은 숫자로 되버린것은 의미없다. 만약 숫자의 정확한 표현이 필요하다면, NumberFormater로 매번 손수 포멧팅하길 원할지도 모르겠다. 특별히 통화를 다루는 API는 종종 정수형 값으로 센트 숫자를 보낸고(안전하게 반올림될 수 있) 100.0으로 나눠서 달러 값을 얻어낸다.있다

이제 반대로도 할수 있다. 다음으로 Decodable 프로토콜 요구사항에대한 구현을 작성해보자.

커스텀 디코딩
디코딩은 원래 다른 생성자를 작성하는 것이다.
extension Beer {
   init(from decoder: Decoder) throws {

   }
}
이번에도 디코더에서 컨테이너를 만들어야한다.
let container = try decoder.container(keyedBy: CodingKeys.self)
모든 기본 프로퍼티를 디코딩할 수 있다. 각각의 경우에서 우리는 기대하는대로 타입을 지정해야한다. 만약 타입이 매치가 안된다면 DecodingError.TypeMismatch가 던져지며 무슨 문제인지 알려주는 정보를 받는다.
let name = try container.decode(String.self, forKey: .name)

let abv = try container.decode(Float.self, forKey: .abv)

let brewery = try container.decode(String.self,
     forKey: .brewery)

let style = try container.decode(BeerStyle.self,
     forKey: .style)

let createdAt = try container.decode(Date.self,
     forKey: .createdAt)

let comments = try container.decodeIfPresent(String.self,
     forKey: .comments)
같은 메소드를 bottleSizes 배열에도 상ㅇ할 수 있는데, 비슷한 방법으로 각 값을 처리할 수 있다. 여기, 새로운 인스턴스에 저장하기 전에 값을 반올림한다.
var bottleSizesArray = try container.nestedUnkeyedContainer(forKey: .bottleSizes)

var bottleSizes: [Float] = []

while (!bottleSizesArray.isAtEnd) {
   let size = try bottleSizesArray.decode(Float.self)
   bottleSizes.append(size.rounded())
}
컨테이너에 더이상 엘리먼트가 없을때까지 계속 값을 디코딩할 것이다.

이제 모든 이 변수들이 정의되고, 디폴트 생성자 호출에대한 모든 대답을 가지게 되었다.
self.init(name: name,
             brewery: brewery,
             abv: abv,
             style: style,
             createdAt: createdAt,
             bottleSizes: bottleSizes,
             comments: comments)
encode(to encoder:)init(from decoder:)의 커스텀 구현으로, JSON 결과를 우리 타입에 맵핑시키는데 더욱 컨트롤할 수 있게 되었다.

오브젝트 평평하게 만들기(Flattening Objects)
JSON이 우리가 고려하지 못했던 중첩 수준을 가진다면 어떻게 될까? 위 예제를 수정하여 abvstyle이 이렇게 표현되도록 해보자.
{
  "name": "Lawnmower",
  "info": {
    "style": "kolsch",
    "abv": 4.9
  }
  // ...
}
이 구조로 동작하려면 인코딩 디코딩 구현을 모두 커스터마이징 해야한다.

중첩된 키를 위해 열거형을 정의(하고 메인 CodingKeys 열거형으로부터 제거)하면서 시작해 볼 것이다.
struct Beer : Codable {
  enum CodingKeys: String, CodingKey {
     case name
     case brewery
     case createdAt = "created_at"
     case bottleSizes = "bottle_sizes"
     case comments
     case info // <-- NEW

  }

  case InfoCodingKeys: String, CodingKey {
     case abv
     case style
  }
}
우리가 값을 인코딩할때 제일 먼저 info 컨테이너에 참조를 넣어야한다. (which if you recall is a keyed container)
func encode(to encoder: Encoder) throws {


     var container = encoder.container(
         keyedBy: CodingKeys.self)
     var info = try encoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
     try info.encode(abv, forKey: .abv)
     try info.encode(style, forKey: .style)

     // ...

}
디코딩가능한 구현을 만들기 위해 반대로도 할 수 있다.
init(from decoder: Decoder) throws {
   let container = try decoder.container(
         keyedBy: CodingKeys.self)

   let info = try decoder.nestedContainer(
         keyedBy: InfoCodingKeys.self)
   let abv = try info.decode(Float.self, forKey: .abv)
   let style = try info.decode(BeerStyle.self,
         forKey: .style)

   // ...

}
이제 인코딩된 포멧에 중첩된 구조를가 가질 수 있게 되었으나 우리 오브젝트에서는 평평하게(flatten) 만들었다.

자식 오브젝트 만들기
brewery가 간단한 문자열로 전달되고 분리된 Brewery 타입을 유지하고 싶다고 해보자.
{
  "name": "Endeavor",
  "brewery": "Saint Arnold",
  // ...
}
이 경우에는 다시 encode(to encoder:)init(from decoder:) 의 커스텀 구현을 해줘야한다.
func encode(to encoder: Encoder) throws {
     var container = encoder.container(keyedBy:
         CodingKeys.self)

     try encoder.encode(brewery.name, forKey: .brewery)

     // ...


}


init(from decoder: Decoder) throws {
     let container = try decoder.container(keyedBy:
         CodingKeys.self)
     let breweryName = try decoder.decode(String.self,
         forKey: .brewery)
     let brewery = Brewery(name: breweryName)

     // ...

}

상속
아래 클래스들을 가진다고 생각해보자.
class Person : Codable {
   var name: String?

}


class Employee : Person {
   var employeeID: String?

}
Person 클레스를 상속하여 Codable을 따르게 되었으나, Employee 인스턴스를 인코딩하려면 무슨일이 일어날까?
let employee = Employee()
employee.employeeID = "emp123"
employee.name = "Joe"


let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted

let data = try! encoder.encode(employee)
print(String(data: data, encoding: .utf8)!)

{
  "name" : "Joe"
}
우리가 원하던 결과는 아니다. 자동으로 생성된 구현이 자식클래스에는 잘 동작하지 않는다. 따라서 다시 인코드/디코드 메소드를 커스터마이징 해야한다.
class Person : Codable {
   var name: String?



   private enum CodingKeys : String, CodingKey {
       case name
   }

   func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(name, forKey: .name)
   }
}
자식클래스에도 같은 일을 할 것이다.
class Employee : Person {
   var employeeID: String?


   private enum CodingKeys : String, CodingKey {
       case employeeID = "emp_id"
   }

   override func encode(to encoder: Encoder) throws {
       var container = encoder.container(keyedBy: CodingKeys.self)
       try container.encode(employeeID, forKey: .employeeID)
   }
}
이렇게 된다.
{
  "emp_id" : "emp123"
}
흠, 이것도 원하던 결과는 아니다. 우리는 부모클래스 구현의 encode(to:)으로 흘러가야한다.

당신은 그냥 부모클래스를 호출하여 인코더에서 넘겨줄 생각일 수 있다. 이것도 동작은 하지만, 현재 스냅샷은 EXC_BAD_ACCESS을 낸다. 내 생각엔 버그같고, 나중에 고쳐질 것이다.

만약 위에처럼 했다면, 같은 컨테이너하에 합쳐진 속성들을 얻을 수 있다. 그러나 스위프트팀은 여러 타입에대해 같은 컨테이너를 재사용하는 것에대해 이야기한다.
만약 공유되는 컨테이너가 필요하면 여전히 super.encode(to: encoder)과 super.init(from: decoder)을 호출할 수 있지만, 우리는 더 안전한 컨테이너화시킨 방법을 추천한다.
그 이유로는, 부모클래스는 우리가 설정한 값을 덮어쓸 수 있지만 우리는 그것에대해 모를 수 있다는 것이다.

대신 우리는 부모클래스의 인코더를 얻기위해 특별한 메소드를 사용할 수 있다. 이 인코더는 이미 컨테이너에서 가지고 있다.
try super.encode(to: container.superEncoder())
이렇게 된다.
{
  "super" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
이렇게하면 "super"라는 새로운 키에 인코딩된 부모클래스를 만들어낸다. 필요하면 이 키 이름을 커스터마이징 할 수 있다.
enum CodingKeys : String, CodingKey {
  case employeeID = "emp_id"
  case person
}


override func encode(to encoder: Encoder) throws {
  // ...

  try super.encode(to:
     container.superEncoder(forKey: .person))
}
아래의 결과가 나온다.
{
  "person" : {
   "name" : "Joe"
  },
  "emp_id" : "emp123"
}
부모클래스에서 일반적인 구조에 접근하는것은 JSON 파싱을 간단화시킬 수 있고, 어던 경우에는 코드 중복을 줄일 수 있다.

UserInfo
인코딩 디코딩 중에 동작을 변경하거나 오브젝트에 컨텍스트를 제공해야하기위해 커스텀 데이터로 표현해야 한다면, 인코딩 디코딩 중에 사용자 정보(User Info)를 전달할 수 있다.

예를들어 고객을위한 이런 JSON을 만들어주는 버전1의 API를 물려받았다고 해보자.
{
  "customer_name": "Acme, Inc",  // old key name
  "migration_date": "Oct-24-1995", // different date format?
  "created_at": "1991-05-12T12:00:00Z"
}
여기서 우리는 created_at 필드와 다른 날짜 양식을 가지는 migration_date 필드를 가지고 있다. 그리고 이름 프로퍼티가 그냥 name으로 바뀌었다고 가정하자.

이 상황은 이상적이지 않은 상황이 분명하지만, 실제로 일어나고, 종종 더러운 API를 물려받기도 한다.

우리를위해 중요한 값들을 담아둘 특별한 사용자 정보 구조체를 정의하자.
struct CustomerCodingOptions {
  enum ApiVersion {
     case v1
     case v2
  }
  let apiVersion = ApiVersion.v2
  let legacyDateFormatter: DateFormatter

  static let key = CodingUserInfoKey(rawValue: "com.mycompany.customercodingoptions")!

}
이제 이 구조체의 인스턴스를 만들어서 인코더와 디코더에 보낼 수 있다.
let formatter = DateFormatter()
formatter.dateFormat = "MMM-dd-yyyy"

let options = CustomerCodingOptions(apiVersion: .v1, legacyDateFormatter: formatter)

encoder.userInfo = [ CustomerCodingOptions.key : options ]


// ...
encode 메소드 안의 모습이다.
func encode(to encoder: Encoder) throws {    var container = encoder.container(keyedBy: CodingKeys.self)


    // here we can require this be present...


    if let options = encoder.userInfo[CustomerCodingOptions.key] as? CustomerCodingOptions {



        // encode the right key for the customer name


        switch options.apiVersion {


        case .v1:


            try container.encode(name, forKey: .legacyCustomerName)


        case .v2:


            try container.encode(name, forKey: .name)

        }


        // use the provided formatter for the date


        if let migrationDate = legacyMigrationDate {


            let legacyDateString = options.legacyDateFormatter.string(from: migrationDate)


            try container.encode(legacyDateString, forKey: .legacyMigrationDate)

        }


    } else {

        fatalError("We require options")

    }



    try container.encode(createdAt, forKey: .createdAt)

}
디코드 생성자에도 정확하게 같은 것을 할 수 있다.

바깥으로부터 옵션을 제공받아 파싱을 더욱 컨트롤할 수 있는 좋은 방법이다. 게다가 DateFormatter같이 생성할때 비싼 오브젝트를 재사용할 수 있다.

다이나믹 코딩 키(Dynamic Coding Keys)
지금까지 이 가이드에서는 스위프트 네이밍과 다를때 코딩키를 표현하려고 enum을 사용했었다. 가끔 이것이 불가능할 수도 있다. 아래의 경우를 생각해보자.
{
  "kolsh" : {
   "description" : "First only brewed in Köln, Germany, now many American brewpubs..."
  },
  "stout" : {
   "description" : "As mysterious as they look, stouts are typically dark brown to pitch black in color..."
  }
}
beer 스타일의 리스트이나, 키들은 실제로 스타일의 이름이다. API는 시간이 지나면서 바뀌고 커질 수 있기 때문에 모든 가능한 상황을 표현할 수 없을 수 있다.

대신에 우리는 CodingKey의 더 다이나믹한 구현을 만들 수 있다.
struct BeerStyles : Codable {
  struct BeerStyleKey : CodingKey {
   var stringValue: String
   init?(stringValue: String)? {
     self.stringValue = stringValue
   }
   var intValue: Int? { return nil }
   init?(intValue: Int) { return nil }

   static let description = BeerStyleKey(stringValue: "description")!

  }

  struct BeerStyle : Codable {
   let name: String
   let description: String
  }

  let beerStyles : [BeerStyle]
}
CodingKeyStirngInt 값 프로퍼티를 둘 다 필요로하고 생성자를 필요로한다. 그러나 이 경우 정수 키를 지원하지 않아도 된다. 또한 스태틱 "destcription 속성을 위한 스태틱 키를 정의했었는데, 바뀌지 않을 것이다.

디코딩을 하며 시작해보자.
init(from decoder: Decoder) throws {
   let container = try decoder.container(keyedBy: BeerStyleKey.self)

   var styles: [BeerStyle] = []
   for key in container.allKeys {
       let nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       let description = try nested.decode(String.self,
           forKey: .description)
       styles.append(BeerStyle(name: key.stringValue,
           description: description))
   }

   self.beerStyles = styles
}
여기서 우리는 컨테이너 안에서 찾은 모든 키들을 다이나믹하게 돌고, 그 키에대한 컨테이너 참조를 잡아둔다. 그리고 여기서 description을 뽑아낸다.

namedescription을 사용하여 직접 BeeryStyle 인스턴스를 생성하고 배열에 추가할 수 있다.

인코딩의 경우는 어떨까?
func encode(to encoder: Encoder) throws {
   var container = try encoder.container(keyedBy: BeerStyleKey.self)
   for style in beerStyles {
       let key = BeerStyleKey(stringValue: style.name)!

       var nested = try container.nestedContainer(keyedBy: BeerStyleKey.self,
           forKey: key)
       try nested.encode(style.description, forKey: .description)
   }
}
여기서 우리는 배열에 있는 모든 스타일들을 돌고, 스타일 이름을 위해 키를 만들어, 키에 컨테이너를 만든다. 그런다음 그 컨테이너에 description을 인코딩하기만 하면 끝난다.

우리가 볼 수 있듯, 커스텀 CodingKey를 만드는 것으로 우리가 다룰 수 있는 응답들의 타입에 많은 유연함을 제공해준다.

에러 처리
지금까지 우리는 어떠한 에러도 다루지 않았다. 이것들은 우리가 실행시킬지도 모르는 에러들이 몇개 있다. 각각은 연관된 값을 제공한다(DecodingError.Context는 언제, 어떤  문제가 생겼는지 알려주는 디버깅 설명을 제공해준다).
  • DecodingError.dataCorrupted(Context): 이 데이타는 오염되었다(즉, 우리가 생각한대로 생기지 않았다). 이것은 여러분이 디코더에 제공한 data가 JSON이 전혀 아니지만, 아마도 실패한 API 콜에서 HTML 에러 페이지일 수 있다.
  • DecodingError.keyNotFound(CodingKey, Context): 필요한 키가 발견되지 않았다. 이것은 질문에서 키를 보냈고 컨텍스트는 어디서, 왜 이런 일이 일어났는지에대한 정보를 제공한다. 이것을 받아다가 몇몇 키를 위한 fallback value를 적절하게 줄 수 있다.
  • DecodingError.typeMismatch(Any.Type, Context): 한 타입을 기대했지만 다른것을 찾았다. 아마도 그 데이터 포멧이 첫번째 버전의 API에서 변경되었을 것이다. 이 에러를 잡고 다른 타입을 사용한 데이터 찾아볼 수 있다.
인코더와 디코더에서 나온 에러들은 문제를 진단하는데 굉장히 유용하며, 특정 상황에 다이나믹하게 적용시킬 수 있는 유연함을 제공하여 적절하게 이것들을 다룰 수 있다.

한가지 이런 예로서 옛날 버전의 API 응답을 마이그레이션하는 것이다. 예를들어 디스크 어딘가에 영속의 캐시에 넣으려고 한 버전의 오브젝트를 인코딩했다고 하자. 나중에 포멧이 바뀌었지만 디스크의 자료는 그대로 있다. 이것을 로드해오려할때 에러가 발생할 수 있는데, 깔끔하게 새로운 데이터 포멧에 마이그레이션하기위해 처리할 수 있다.

더 읽을거리
  • Codable.swift: 스위프트가 오픈소스화되어서 좋은 점 중 하나는 이것이 어덯게 구현되었는지 그냥 볼 수 있다는 점이다. 꼭 한번 보자!
  • Using JSON with Custom Types: 애플이 제공하는 playground 샘플인데, 더 복잡한 JSON 파싱 시나리오를 볼 수 있다.

결론
여기까지 새로운 스위프트 4의 Codable API를 어떻게 사용하는지에대해 빠르게 훑어보았다. 추가하고 싶은게 있나? 아래에 댓글을 달아달라.

이 작업이 마음에 들었는가? 그렇다면 NSScreencast도 마음에 들것이라 생각된다.



이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: What's new in Swift 4.0

This article was translated with permission from the English original, What's new in Swift 4.0? on Hacking with Swift .

새로운 스위프트4의 새로운 점을 배우기위한 실제 예제 코드: 새로운 인코딩과 디코딩, 똑똑해진 키패스(keypaths), 다열 문자열(multi-line string) 등이 있습니다!

스위프트4.0이 모두의 인기있는 앱 개발 언어로 새로 배포되었고, 간단하고 안전한 코드를 작성할 수 있는 다양한 기능들을 소개했다. 스위프트 3.0이 발표되었을때 그 극적인 변화에 비하면 즐겁게 받아드릴 수 있을 것인데, 실제로 대부분 변화는 현재 존재하는 스위프트 코드와 완전히 호환이 되게 만든 것이다. 따라서 여러분이 약간의 변경사항을 원한다면, 그렇게 오래 걸리지 않을 것이다.

주의: 스위프트4는 아직 활발히 개발중이다. 나는 여기서 몇가지 유용한 새 기능을 골랐고, 이것들은 현재 모두 구현되었으며 시도해볼 수 있다. 마지막 배포가 있기 전의 그 달에 더 많은 기능이 나올 것임을 기억하라.

이 글이 마음에 들었다면, 아래의 것들도 흥미있을 수 있을 것이다.

스위프트스러운 인코딩과 디코딩
값 타입이 멋지다는 사실은 알고있지만, NSCoding과같은 Objective-C API와 인터렉트하기는 힘들다는 것도 알고있다. 둘을 이어주는 레이어를 만들거나 클래스에 넣어 사용해야한다. 두가지 방법 다 좋지만은 않다. 더욱 나쁜것은, 클래스에 넣어 변경할지라도 직접 인코딩과 디코딩 메소드를 작성해야하며, 이것은 고통스러운 에러를 야기할 수 있다.

스위프트4는 특수한 코드 없이 커스텀 데이터 타입을 시리얼라이즈하고 디-시리얼라이즈하게해주는 Codable 프로토콜을 소개했다. 여러분의 값 타입 손실에대해 더이상 걱정하지 않아도 된다. 더 나은점은 어떻게 데이터를 시리얼라이즈 할지 정할 수 있다는 것이다. 기존의 프로퍼티 리스트 양식을 사용할 수도 있고 JSON도 가능하다.

여러분이 읽고 있는 것이 맞는 말이다: 스위프트4는 특별한 코드 없이 여러분의 커스텀 데이터 타입을 JSON으로 시리얼라이즈 해준다.

이 얼마나 아름다운 광경인지 지켜보자. 먼저, 여기에 커스텀 데이터 타입이 있고, 그 인스턴스가 있다.
struct Language: Codable {
   var name: String
   var version: Int
}

let swift = Language(name: "Swift", version: 4)
let php = Language(name: "PHP", version: 7)
let perl = Language(name: "Perl", version: 6)
Language 구조체에 Codable 프로토콜을 따르게 한 것을 볼 수 있을 것이다. 이 간단한 추가사항만으로 아래처럼 JSON의 Data 표현으로 변환할 수 있다.
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(swift) {
   // save encoded somewhere
}
스위프트는 자동으로 여러분의 데이터 타입 안에있는 모든 프로퍼티들을 인코딩할 것이다. 여러분은 딱히 할 일이 없다.

이제 여러분도 나와같이 오랫동안 NSCoding을 사용해왔다면 뭔가 의심스러울 것이다. 정말로 제대로 동작할까? 그리고 이게 동작한다는 것을 어떻게 확신할 수 있을까? 음, Data 오브젝트를 문자열로 만드는 코드를 추가해서 한번 출력해보자. 그리고 다시 Language 인스턴스로 디코딩 시켜, 읽을 수 있는지 보자.
if let encoded = try? encoder.encode(swift) {
   if let json = String(data: encoded, encoding: .utf8) {
       print(json)
   }

   let decoder = JSONDecoder()
   if let decoded = try? decoder.decode(Language.self, from: encoded) {
       print(decoded.name)
   }
}
디코딩이 타입캐스팅을 필요로 하지 않는다는 점을 인지하자. 첫번째 파라미터로 데이터 타입 이름을 주면, 스위프트는 여기서 리턴 타입을 추론한다.

JSONEncoder와 그 프로퍼티 리스트에 짝인 PropertyListEncoder는 이 부분이 어떻게 동작할지 수많은 커스터마이징 옵션을 가진다. 빽빽한(compact) JSON을 원하는지, 출력하지 좋은(pretty-printed) JSON을 원하는지, ISO8601 데이터를 원하는지, 유닉스 epoch 데이터를 원하는지, 바이너리 프로퍼티 리스트를 사용하고 싶은지 XML을 사용하고 싶은지 정할 수 있다. 다른 옵션들에대한 더 많은 정보를 원한다면  the Swift Evolution proposal for this new feature을 확인해보자.

다열 문자열 리터럴
스위프트에서 다열 문자열로 쓰려면 항상 문자열 안에 \n을 써서 개행을 했었어야 했다. 코드상에서 보기에 좋아보이지 않지만 사용자에게는 올바르게 보여진다. 다행히 스위프트4에서 새로 소개된 다열 문자열 리터럴 문법은 문자열 보간법같은 기능도 쓸 수 있는 채로, 자유롭게 개행을 할 수 있게하고 이스케이핑 없이 따옴표를 사용할 수 있다.

문자열 리터럴을 시작하기 위해서는 쌍따옴표를 3개(""") 적고 리턴키를 누른다. 그 다음 여러분이 원하는 만큼 문자열을 쳐내는데, 변수나 개행도 가능하며, 문자열이 끝날때는 리턴키를 누르고 쌍따옴표를 세개 치면 된다.

문자열 리터럴에는 두가지 중요한 규칙이 있기 때문에 리턴키를 누르는 것에대해 이야기하고 싶다. """으로 문자열을 열때, 여러분의 문자열 내용은 반드시 새로운 라인에서 시작해야한다. 그리고 """로 다열 문자열을 끝낼때는 새로운 줄에서 """을 쳐야한다.

여기에 예시가 있다.
let longString = """
When you write a string that spans multiple
lines make sure you start its content on a
line all of its own, and end it with three
quotes also on a line of their own.
Multi-line strings also let you write "quote marks"
freely inside your strings, which is great!
"""
이것은 정의에서 몇몇 개행이 들어있는 새로운 문자열을 만든 것이다. 훨씬 읽고 쓰기 쉽다.

더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

키-밸류 코딩을위한 키패스 증진(Improved keypaths for key-value coding)
Objective-C에서 사랑했던 기능 중 하나는 직접 접근하는 것이 아닌 동적으로 프로퍼티를 참조하는 능력이다. 이것은 "주어진 오브젝트 X에 내가 읽고 싶은 프로퍼티가 있어"라고 말할 수 있다. 이런 참조 방법을 키패스(keypath)라 부르고 이것은 직접 프로퍼티에 접근하는 것과 구별되는데, 여기서는 실제로 값을 읽고 쓰는 것이 아니라 슬며시 감춰놨다가 나중에 사용하는 것이다.

여러분이 아직 키패스를 사용해보지 않았다면 기존의 스위프트 메소드를 이용해서 어떤식으로 동작했는지 보여주겠다. Starship이라는 구조체를 하나 정의하고 Crew라는 구조체를 정의하자. 그리고 각각의 인스턴스를 하나씩 만든다.
// an example struct
struct Crew {
   var name: String
   var rank: String
}

// another example struct, this time with a method
struct Starship {
   var name: String
   var maxWarp: Double
   var captain: Crew

   func goToMaximumWarp() {
       print("\(name) is now travelling at warp \(maxWarp)")
   }
}

// create instances of those two structs
let janeway = Crew(name: "Kathryn Janeway", rank: "Captain")
let voyager = Starship(name: "Voyager", maxWarp: 9.975, captain: janeway)

// grab a reference to the goToMaximumWarp() method
let enterWarp = voyager.goToMaximumWarp

// call that reference
enterWarp()
스위프트에서는 함수가 일급 타입이므로 마지막 두 줄에서 goToMaximumWarp() 메소드를 enterWarp으로 참조하게 만들 수 있다. 그리고 나중에 우리가 원할때 언제든지 사용할 수 있다. 여기서 문제는 이와같은 것을 프로퍼티에는 못한다는 것이다. "불가피한 상황이 생겼을때 확인할 수 있도록 captain의 이름 프로퍼티를 참조하는 것을 만들어라"고 말할 수 없다. 스위프트는 단지 직접 프로퍼티를 읽어서 원래의 값을 얻어내가 때문이다.

이것은 키패스로 고칠 수 있는데, 우리의 enterWarp() 코드같은 프러퍼티를 참조하며 호출되지 않은 것(uninvoked references to properties)이다. 이제 참조를 호출하면 현재 값을 얻어낸다. 그러나 나중에 참조를 호출하면 나중에 값을 얻어낸다. 프로퍼티의 수를 통해 파헤쳐볼 수 있고, 스위프트는 올바른 타입을 받나냈는지 확인하기위해 타입 추론을 이용한다.

스위프트 에볼루션 커뮤니티는 이 키패스 문법이 올바른지에대해 꽤 오랫동안 토론해왔었는데, 이것이 스위프트 코드와 시각적으로 달라 보일 필요가 있었기 때문이다. 결국 백슬래시를 사용한 문법이 되었다. \Starship.name, \Starship.maxWarp, \Starship.captain.name. 이 두가지를 변수에 할당하여 여러분이 원하는 언제나 어떤 Starship 인스턴스에서도 사용할 수 있다. 예제를 보자.
let nameKeyPath = \Starship.name
let maxWarpKeyPath = \Starship.maxWarp
let captainName = \Starship.captain.name

let starshipName = voyager[keyPath: nameKeyPath]
let starshipMaxWarp = voyager[keyPath: maxWarpKeyPath]
let starshipCaptain = voyager[keyPath: captainName]
스위프트는 타입 추론이 가능하기 때문에 starshipName 문자열과 starshipMaxWarp 더블형을 만들 것이다. 이 예제의 세번째처럼 프로퍼티의 프로퍼티라도 스위프트는 역시 올바르게 이해할 것이다.

나중에는 런타임중에도 배열 인덱스를 접근할 수 있거나 문자열로부터 키패스를 만들어낼 수 있는 계획이 있다. 더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

약간의 광고
If you're enjoying this article, you might like my free Natural Swift video. It gives you 75 minutes of hands-on coding that teaches functional programming, protocol-oriented programming, and value types, and you can download it for free with no obligation or catches – just click here.

And now back to your regularly scheduled broadcast…

딕셔너리 기능 증진
스위프트4에서 한가지 더 흥미로운 프로퍼절은 딕셔너리를 더욱 강력하게 만들고 특정 상황에 여러분이 예상한대로 동작하게 만드는 기능이었다.

간단한 예제로 시작해보자. 스위프트3에서 딕셔너리를 필터링 하는 것은 새로운 딕셔너리를 반환하지 았다. 대신 키/값 래이블과 함께 튜플 배열을 반환했었다. 예제이다.
let cities = ["Shanghai": 24_256_800, "Karachi": 23_500_000, "Beijing": 21_516_000, "Seoul": 9_995_000];
let massiveCities = cities.filter { $0.value > 10_000_000 }
이후에는 더이상 딕셔너리가 아니므로 massiveCities["Shanghai"]로 읽을 수 없다. 대신 massiveCities[0].value를 사용해야했는데, 별로 좋아보이지 않는다.

스위프트4부터는 더욱 여러분이 생각한대로 동작한다. 여기서는 새로운 딕셔너리를 반환해준다. 명백하게 이것은 튜플-배열 리턴 타입에 의존하고있는 현재 코드를 변경해야 할 것이다.

비슷하게, 딕셔너리에서 map() 메소드도 많은 사람들이 원하던 방식대로 되지 않았었다. 전달되면 키-값 튜플을 받고, 배열에 추가하기위해 한 값을 반환할 수 있다. 예제를 보자.
let populations = cities.map { $0.value * 2 }
이것은 스위프트4에서 바뀌지 않았지만, mapValues()라 불리는 새 메소드가 추가되었다. 이것은 더욱더 유용할 것으로 보이는데, 값을 변경하고 기존의 키를 이용해 딕셔너리에 다시 값을 넣어둘 수 있게 해준다.

예를들어, 이 코드는 모든 도시 인구를 100만으로 나누고 문자열로 만든다. 그리고 다시 Shanghai, Karachi, Seoul의 같은 키로 새로운 딕셔너리에 넣는다.
let roundedCities = cities.mapValues { "\($0 / 1_000_000) million people" }
(여기서 여러분은 실수로 중복할 수도 있기 때문에 딕셔너리 키를 맵핑시키는게 안전하지 않다고 생각할 수 있다.)

쉽게, 내가 좋아하는 새로운 딕셔너리 추가사항은 grouping 생성자이다. 이것은 시퀀스를 여러분이 원하는대로 그룹화한 시퀀스의 딕셔너리로 만들어준다. 계속해서 cities 예제로가서, 도시 이름의 배열을 얻어내기위해 cities.keys를 사용할 수 있으며, 첫글자로 그룹화한다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.characters.first! }
print(groupedCities)
// ["B": ["Beijing"], "S": ["Shanghai", "Seoul"], "K": ["Karachi"]]
대신 아래처럼 그 이름의 길이에따라 도시들을 그룹화 시킬 수 있다.
let groupedCities = Dictionary(grouping: cities.keys) { $0.count }
print(groupedCities)
// [5: ["Seoul"], 7: ["Karachi", "Beijing"], 8: ["Shanghai"]]
마지막으로, 딕셔너리 키에 접근할 수 있고 키에 해당하는 값이 없다면 디폴트 값을 제공할 수 있다.
let person = ["name": "Taylor", "city": "Nashville"]
let name = person["name", default: "Anonymous"]
이제 경험이 좀 있는 개발자들은 nil-coalescing으로 사용하는게 더 났다고 주장할 것이다. 나도 동의한다. 현재 버전의 스위프트것을 사용하는 것 대신에 아래처럼도 쓸 수 있다.
let name = person["name"] ?? "Anonymous"
그러나 그냥 읽는데는 문제가 없지만, 딕셔너리 값을 수정할때는 동작하지 않는다. 적절하게 딕셔너리 값을 수정할 수 없는데, 그 키로 접근하면 옵셔널을 반환하기 때문이다. 키가 존재하지 않을 수도 있다. 스위프트4의 디폴트 딕셔너리 값으로 여러분은 더욱 적합한 코드를 작성할 수 있다. 아래처럼 말이다.
var favoriteTVShows = ["Red Dwarf", "Blackadder", "Fawlty Towers", "Red Dwarf"]
var favoriteCounts = [String: Int]()

for show in favoriteTVShows {
   favoriteCounts[show, default: 0] += 1
}
이 코드는 favoriteTVShows에 있는 모든 문자열을 돌면서, favoriteCounts라는 딕셔너리를 사용하여 각 항목이 나타날때마다 카운팅을 한다. 코드 한줄에서 딕셔너리를 수정할 수 있는데, 딕셔너리에는 항상 값을 가질거라는 것을 알기 때문이다. 디폴트값의 0이든 이전에 카운팅되었던 것에따라 더 높은 숫자든.

더 많은 정보를 원한다면 the Swift Evolution proposal for these new features을 확인해보자.

문자열은 다시 컬랙션이다!
작은 변화지만 많은 사람들을 행복하게 만든다. 문자열이 다시 컬랙션이 되었다. 이 의미는, 이것을 뒤집거나, 문자를 하나씩 돌거나, map()하거나 flatMap()하는 등이 가능하다. 예제를 보자.
let quote = "It is a truth universally acknowledged that new Swift versions bring new features."
let reversed = quote.reversed()

for letter in quote {
   print(letter)
}
이 변경은 String Manifesto라 불리는 다양한 개정사항의 부분에 소개되어 있다.

한쪽만의 범위(One-sided ranges)
가장 최신은 아니지만 최근에, 스위프트4는 파이썬같은 한쪽만 컬랙션 자르기를 소개했다. 한쪽이 빠지면 자동으로 컬랙션의 시작이나 끝을 추론한다. 이 변경은 현재 코드에 영향을 주지 않는다. 현재 연산자에 새로운 부분이기 때문에 잠재적인 손상을 걱정하지 않아도 된다.

아래에 예제가 있다.
let characters = ["Dr Horrible", "Captain Hammer", "Penny", "Bad Horse", "Moist"]
let bigParts = characters[..<3]
let smallParts = characters[3...]
print(bigParts)
// ["Dr Horrible", "Captain Hammer", "Penny"]
print(smallParts)
// ["Bad Horse", "Moist"]
더 많은 정보를 원한다면 the Swift Evolution proposal for this new feature을 확인해보자.

아직 더 남은게 있다...
스위프트4를 탑재한 Xcode의 첫번째 배포판은 6월에 iOS11, tvOS11, watchOS4, macOS와함께 나올것으로 보인다(역자: 이미 나왔습니다). 지금까지 우리가 본 것은 특히 명확하게 이미 나오기로 약속되었고, 팀은 가능한 추가할 수 있도록 스위프트4를 만들려고 노력하고 있다. 주로 기존에 있는 것을 고치거나 수정하지 않도록 새로운 기능을 추가하는 것이 업그레이드하기 편하기 해줄 수 있고, 고맙게도 이 언어를 위한 새로운 안정성의 시작의 신호가 있다.

스위프트 에볼루션이 때론 혼돈속에 빠지기도 했지만(접근 수준같은 부분), 스위프트4는 애플의 커뮤니티 방법을 다시 검증했다. 나는 위에 몇몇 스위프트 에볼루션을 링크 걸어두었는데, 각각의 것들은 커뮤니티 여론의 도움으로 광범위하게 토론되었다. 애플 엔지니어가 변경을 강행하지 않고 분별력있게 하였으며, 무엇이 이미 스마트하고 엘레강트한 언어인지 다듬기위해 고려했다.

한가지 지연된 기능은 ABI 호환성인데, 이것은 개발자들에게 컴파일된 라이브러리를 배포할 수 있게 해줄 것이다. 오늘 스위프트에 몇가지 핵심 기능이 구현되지 않고 남아있다. 다행히도 스위프트5가 되기 전에 만나볼 수 있을 것이다...


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.




WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,
제목: What's New in Swift 4

주의: 이 튜토리얼은 Xcode9 beta 1에 들어있는 스위프트 4버전을 사용함.

스위프트4는 애플이 2017년 가을에 낼 가장 최신의 메이저 배포이다. 그 메인 초점은 스위프트3 코드와 소스 호환성 제공이며, ABI 안정성에 맞춰 작업하고 있다. 이 글에서는 스위프트가 바뀌면서 여러분의 코드에 중요하게 영향을 줄 것을 짚어 줄 것이다. 그리고 이제 시작해보자!


Swift 4Swift 4

시작하며
스위프트4는 Xcode9에 포함되있다. 여러분은 애플의 developer portal에서 최신버전의 Xcode9를 다운받을 수 있다(반드시 활성화된 개발자 계정이 있어야한다). 각 Xcode beta는 배포될 시점의 가장 최신의 스위프트4를 탑재할 것이다.

읽어내려가다보면 [SE-xxxx] 양식의 링크를 볼 수 있을 것이다. 이 링크들은 관련된 스위프트 에볼루션 프로포절로 연결해준다. 어떤 주제든 더 배우고 싶으면 이 링크를 확인해보자.

나는 여러분이 스위프트4의 각 기능을 시도해보길 추천하며 플레이그라운드에서 돌려보길 바란다. 그리하여 지식을 여러분 머릿속에 확립시킬 수 있고 각 주제마다 더 깊게 들어가볼 수 있게 해줄 것이다. 각 예제를 고쳐보고 추가해보면서 가지고 놀아보자. 잘 즐겨보길 바란다!

노트: 이 글은 각 Xcode beta에대해 갱신될 것이다. 여러분이 다른 스위프트 스넵샷을 사용하면 여기 코드가 동작할것이라는 보장은 할 수 없다.

스위프트4로 마이그레이션하기
스위프트3에서 4로 마이그레이션하는 것은 2.2에서 3으로 가는 것보다 덜 힘들 것이다. 보통 많은 변화가 추가되고 개별적으로 건드릴 필요가 없다. 그덕에 스위프트 마이그레이션 툴은 주요 변화만 처리해 줄 것이다.

Xcode9는 동시에 스위프트4와 스위프트3.2안에 스위프트3도 가능하게 지원한다. 여러분 프로젝트에서 각 타겟은 스위프트3.2나 스위프트4가 될 수 있고, 필요에따라 조각조각 마이그레이션 할 수 있다. 그래도 스위프트3.2로 변환하는 것은 완전히 자유롭진 않다. 새로운 SDK에 맞추기위해 여러분의 코드 부분을 업데이트 해야할 것이다. 그리고 스위프트는 아직 ABI 안정이 되지 않았기 때문에 Xcode9로 여러분의 의존성들을 다시 컴파일 해야할 것이다.

스위프트4로 마이그레이션할 준비가 되었다면 Xcode는 다시한번 마이그레이션 툴로 여러분을 도와줄 것이다. Xcode에서 Edit/Convert/To Current Swift Syntax...로 가서 변환 툴을 실행시킨다.

변환하고 싶은 타겟을 선택한 후, Xcode는 Objective-C 추론에대한 설정을 물어볼 것이다. 추론을 제한하여 바이너리 크기를 줄이는 추천 옵션을 선택하자(이 토픽에대해 더 알고 싶으면 아래에 Limiting @objc Inference를 확인해보자).

여러분의 코드가 의도한대로 바뀌었는지 잘 이해하기위해 우리는 제일 먼저 스위프트4의 API 변경사항들을 다룰 것이다.

API 변경사항
스위프트4에서 소개된 추가사항에 들어기보기 전에, 기존의 API를 변경하거나 증진시킨것이 무엇이 있는지 먼저 살펴보자.

문자열(String)
스위프트4에서 String은 상당히 많은 사랑을 받고 있다. 이 프로포절은 많은 변경사항을 가지며, 큰것부터 하나씩 보자. [SE-0163]

여러분은 이 부분에서 옛날 기분이 들것이다. 스위프트가 2.0버전으로 돌아와서 다시 컬랙션이 되었다. String에서 characters 배열이 필요하다는 점을 없앴다. 이제 String 오브젝트를 직접 이터레이트(iterate) 할 수 있다.
let galaxy = "Milky Way 🐮"
for char in galaxy {
  print(char)
}

YesYes


String으로 논리적인 이터레이트만 가능한게 아니라, SequenceCollection의 모든 기능을 사용할 수 있다.
galaxy.count       // 11

galaxy.isEmpty     // false

galaxy.dropFirst() // "ilky Way 🐮"

String(galaxy.reversed()) // "🐮 yaW ykliM"


// Filter out any none ASCII characters
galaxy.filter { char in
  let isASCII = char.unicodeScalars.reduce(true, { $0 && $1.isASCII })
  return isASCII
} // "Milky Way "
위의 ASCII 예제는 Character의 작은 개선사항을 설명해준다. 이제 Character에서 직접 UnicodeScalarView에 접근할 수 있다. 이전에는 새 String을 만들어야 했었다 [SE-0178].

다른 추가사항은 StringProtocol이다. 이전에 String에 정의되있던 많은 기능들이 이 프로토콜에 정의되있다. 이렇게 변경하게된 이유는 자르기(slice) 작업을 개선하기 위함이다. 스위프트4는 String에서 서브시퀀스를 참조하기위해 Substring 타입을 추가했다.

StringSubstring은 둘 다 StringProtocol에 주어진 기능을 대부분 비슷하게 구현하였다.
// Grab a subsequence of String
let endIndex = galaxy.index(galaxy.startIndex, offsetBy: 3)
var milkSubstring = galaxy[galaxy.startIndex...endIndex]   // "Milk"

type(of: milkSubstring)   // Substring.Type


// Concatenate a String onto a Substring
milkSubstring += "🥛"     // "Milk🥛"


// Create a String from a Substring
let milkString = String(milkSubstring) // "Milk🥛"
또다른 멋진 개선사항은 String이 grapheme cluster를 해석하는 방식이다. 이 해결방안은 유니코드9 각색한 것으로부터 나왔다. 이전에는 유니코드 문자들이 여러 코드 포인트로 만들어져있었는데, 이때문에 count는 1로 합산되었다. 피부톤을 선택하고 이모티콘을 넣으면 이런 일이 발생했다. 아래에 몇가지 예제가 있는데, 예전의 동작방식과 이후의 동작방식을 보여준다.
"👩💻".count // Now: 1, Before: 2

"👍🏽".count // Now: 1, Before: 2

"👨❤️💋👨".count // Now: 1, Before, 4
이것은 String Manifesto에 언급된 변화들중 일부분이다. 나중에는 여러분이 예상했던대로 이것에대한 모든 동기와 제안된 솔루션에대해 읽을 수 있다.

딕서녀리와 셋(Dictionary and Set)
Collection 타입이 가버린다면(As far as Collection types go), SetDictionary은 항상 직관적이지 않았다. 운좋게도 스위프트 팀은 여기에 애정을 가져주었다 [SE-0165].

생성자 기반의 시퀀스
그것들 중 첫번째는 키-값 쌍(튜플)의 시퀀스에서 딕셔너리를 만들 수 있게 해주었다는 점이다.
let nearestStarNames = ["Proxima Centauri", "Alpha Centauri A", "Alpha Centauri B", "Barnard's Star", "Wolf 359"]
let nearestStarDistances = [4.24, 4.37, 4.37, 5.96, 7.78]

// Dictionary from sequence of keys-values
let starDistanceDict = Dictionary(uniqueKeysWithValues: zip(nearestStarNames, nearestStarDistances))
// ["Wolf 359": 7.78, "Alpha Centauri B": 4.37, "Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Barnard's Star": 5.96]

중복 키 해결법
이제 여러분이 원하는 방식으로 중복키로 딕셔너리를 초기화할 수 있게 되었다. 이렇게하여 이 문제에대해 별 말 없이도 키-값 쌍을 덮어쓰는 것을 막을 수 있다.
// Random vote of people's favorite stars
let favoriteStarVotes = ["Alpha Centauri A", "Wolf 359", "Alpha Centauri A", "Barnard's Star"]

// Merging keys with closure for conflicts
let mergedKeysAndValues = Dictionary(zip(favoriteStarVotes, repeatElement(1, count: favoriteStarVotes.count)), uniquingKeysWith: +) // ["Barnard's Star": 1, "Alpha Centauri A": 2, "Wolf 359": 1]
위 코드에서는 충돌하는 두 값을 더하여 중복 키 문제를 해결하기위해 축약된 +와 함께 zip을 사용했다.

노트: zip이 생소하다면 애플의 Swift Documentation를 빠르게 읽어보자.

필터링
이제 DictionarySet 모두 필터하였을때 원래 타입의 새 오브젝트를 결과로 할 수 있게 되었다.
// Filtering results into dictionary rather than array of tuples
let closeStars = starDistanceDict.filter { $0.value < 5.0 }
closeStars // Dictionary: ["Proxima Centauri": 4.24, "Alpha Centauri A": 4.37, "Alpha Centauri B": 4.37]

딕셔너리 맵핑
Dictionary는 그 값을 바로 맵핑할 수 있는 유용한 메소드를 얻어냈다.
// Mapping values directly resulting in a dictionary
let mappedCloseStars = closeStars.mapValues { "\($0)" }
mappedCloseStars // ["Proxima Centauri": "4.24", "Alpha Centauri A": "4.37", "Alpha Centauri B": "4.37"]

딕셔너리의 디폴트 값
일반적으로 Dictionary의 값에 접근할 때, 값이 nil인 경우 디폴트 값을 지정하기위해 nil-coalescing 연산자를 사용한다. 스위프트4에서는 그 줄에서 변경하여 더 깔끔하고 멋진 방법으로 되었다.
// Subscript with a default value
let siriusDistance = mappedCloseStars["Wolf 359", default: "unknown"] // "unknown"


// Subscript with a default value used for mutating
var starWordsCount: [String: Int] = [:]
for starName in nearestStarNames {
  let numWords = starName.split(separator: " ").count
  starWordsCount[starName, default: 0] += numWords // Amazing

}
starWordsCount // ["Wolf 359": 2, "Alpha Centauri B": 3, "Proxima Centauri": 2, "Alpha Centauri A": 3, "Barnard's Star": 2]
이전에 이 타입을 변경하려면 커다란 if let문으로 감싸야 했다. 스위프트4는 한줄에 된다!

딕서녀리 그룹핑
또다른 멋진 추가사항은 Dictionary를 Sequence로부터 초기화할 수 있는 기능과 대괄호로 그룹화할 수 있는 기능이다.
// Grouping sequences by computed key
let starsByFirstLetter = Dictionary(grouping: nearestStarNames) { $0.first! }

// ["B": ["Barnard's Star"], "A": ["Alpha Centauri A", "Alpha Centauri B"], "W": ["Wolf 359"], "P": ["Proxima Centauri"]]
이렇게하면 데이터를 특정 패턴으로 그룹화할때 편하게 다가올 것이다.

용량 잡아놓기(Reserving Capacity)
SequenceDictionary 둘 다 이제 명시적으로 용량을 정해놓을 수 있다.
// Improved Set/Dictionary capacity reservation
starWordsCount.capacity  // 6

starWordsCount.reserveCapacity(20) // reserves at _least_ 20 elements of capacity

starWordsCount.capacity // 24
이 타입에서 재할당은 부담되는 작업일 수 있다. 저장되는데 필요한 데이터가 얼마인지 알때 reserveCapacity(_:)을 사용하여 퍼포먼스를 쉽게 개선할 수 있다.

이것은 엄청난 양의 정보이니, 그 타입들에대해 명확하게 확인해보고 추가적으로 여러분의 코드에 사용할 방법이 있는지 찾아보자.

private 접근 수정자
스위프트3애서 그렇게 반갑지 않던 부분은 fileprivate 였을 것이다. 이론적으로는 좋지만, 실제로는 사용하기 혼란스러울 수 있었다. private의 목표는 맴버 자신에서만 사용하는 것이었는데, fileprivate은 같은 파일안에 맴버들사이에서 접근을 공유하려는 경우에는 거의 사용하지 않는다.

이 이슈는 스위프트가 익스텐션을 사용하여 논리적인 그룹으로 코드를 쪼개도록 지향하기 때문에 생긴다. 익스텐션은 원래 맴버가 선언된 범위 밖에서 사용할때가 많다. 그렇기때문에 fileprivate에대한 확장이 필요하게 되었다.

스위프트4는 한 타입과 다른 익스텐션 사이에 같은 접근 제어 범위를 공유하여 원래 의도를 이해한다. 이것은 오직 같은 소스파일 안에서만 잡아둔다 [SE-0169].

struct SpaceCraft {
  private let warpCode: String

  init(warpCode: String) {
   self.warpCode = warpCode
  }
}

extension SpaceCraft {
  func goToWarpSpeed(warpCode: String) {
   if warpCode == self.warpCode { // Error in Swift 3 unless warpCode is fileprivate

     print("Do it Scotty!")
   }
  }
}

let enterprise = SpaceCraft(warpCode: "KirkIsCool")
//enterprise.warpCode  // error: 'warpCode' is inaccessible due to 'private' protection level
enterprise.goToWarpSpeed(warpCode: "KirkIsCool") // "Do it Scotty!"
그리하여 fileprivate는 코드 구조를 위한 임시목적이라기보단 의도한 목적에 맞게 사용할 수 있다.

API 추가사항들
이제 스위프트4의 반짝반짝 빛나는 새 기능을 살펴보자. 이 변경사항들은 현재 코드를 망가뜨리진 않고 간단하게 추가할 수 있다.

아키브와 시리얼라이즈


지금까지 스위프트는, 커스텀타입을 시리얼라이즈와 아키브하기위해서 여러노력을 했었어야 했다. class 타입의 경우 NSObject를 상속하고 NSCoding 프로토콜을 구형해야했다.

structenum같은 값 타입은 꼼수처럼 NSObjectNSCoding을 확장한 보조 오브젝트를 만들어야 했다.

스위프트4는 세가지 타입 모두 시리얼라이즈 할 수 있게하여 문제를 해결해 주었다 [SE-0166].

struct CuriosityLog: Codable {
  enum Discovery: String, Codable {
   case rock, water, martian
  }

  var sol: Int
  var discoveries: [Discovery]
}

// Create a log entry for Mars sol 42
let logSol42 = CuriosityLog(sol: 42, discoveries: [.rock, .rock, .rock, .rock])
이 예제에서 볼 수 있듯, EncodableDecodable을 위해 Codable 프로토콜만 구현하면 된다. 모든 프로퍼티가 Codable이면 그 프로토콜은 컴파일러예의햬 자동으로 생성된다.

실제로 오브젝트를 인코드하기 위해, 이것을 인코더에 보내야 할 것이다. 스위프트 인코더는 스위프트4에서 활발하게 구현되고있다. 각각의 다른 스킴에따라 오브젝트를 인코딩할 수 있다 [SE-0167]. (주의: 이 제안중 일부는 아직 개발중에 있다)

let jsonEncoder = JSONEncoder() // One currently available encoder


// Encode the data
let jsonData = try jsonEncoder.encode(logSol42)
// Create a String from the data
let jsonString = String(data: jsonData, encoding: .utf8) // "{"sol":42,"discoveries":["rock","rock","rock","rock"]}"
한 오브젝트를 받아서 자동으로 JSON 오브젝트로 인코딩 시킨다. 속성들을 확인하기위해 JSONEncoder를 커스터마이징된 아웃풋으로 만든다.

이 프로세스의 마지막 과정은 데이터를 구체적인 오브젝트로 디코딩하는 것이다.
let jsonDecoder = JSONDecoder() // Pair decoder to JSONEncoder


// Attempt to decode the data to a CuriosityLog object
let decodedLog = try jsonDecoder.decode(CuriosityLog.self, from: jsonData)
decodedLog.sol         // 42

decodedLog.discoveries // [rock, rock, rock, rock]
스위프트4에서의 인코딩과 디코딩은 @objc 프로토콜의 오버헤드나 한계에 의존하지 않고서 스위프트로부터 기대되는 타입 세이프티를 취할 수 있다.

키-값 코딩
지금까지 스위프트에서 함수는 클로저이기 때문에 함수를 호출하지 않은채 참조하여 붙잡아둘 수 있었다. 그렇지만 프로퍼티가 데이터를 잡아두어 실제로 접근하고 있지 않는한 함수를 프로퍼티에 참조하여 잡아둘 수 없었다.

스위프트4에서 아주 흥미로운 추가사항은 인스턴스의 값에서도 get/set으로 타입의 키패스를 참조할 수 있다는 점이다.
struct Lightsaber {
  enum Color {
   case blue, green, red
  }
  let color: Color
}

class ForceUser {
  var name: String
  var lightsaber: Lightsaber
  var master: ForceUser?

  init(name: String, lightsaber: Lightsaber, master: ForceUser? = nil) {
   self.name = name
   self.lightsaber = lightsaber
   self.master = master
  }
}

let sidious = ForceUser(name: "Darth Sidious", lightsaber: Lightsaber(color: .red))
let obiwan = ForceUser(name: "Obi-Wan Kenobi", lightsaber: Lightsaber(color: .blue))
let anakin = ForceUser(name: "Anakin Skywalker", lightsaber: Lightsaber(color: .blue), master: obiwan)
이것은 몇 ForceUser 인스턴스를 그 name, lightsaber, master로 설정하여 생성하는 코드이다. 키패스를 만들기위해서는 필요한 프로퍼티 앞에 백슬래시(\)만 넣어주면 된다.
// Create reference to the ForceUser.name key path
let nameKeyPath = \ForceUser.name

// Access the value from key path on instance
let obiwanName = obiwan[keyPath: nameKeyPath]  // "Obi-Wan Kenobi"
이 예제에서는 ForceUsername 프로퍼티 키패스를 생성했다. 그리고 keyPath라는 새로운 서브스크립트에 이 키패스를 담아서 사용한다. 이제 디폴트에의해 이 서브스크립트는 모든 타입에서 사용할 수 있다.

아래에는 하위 오브젝트 예제, 프로퍼티 설정, 키패스 참조 만들기에대한 여러가지 키패스 사용법 예시가 있다.
// Use keypath directly inline and to drill down to sub objects
let anakinSaberColor = anakin[keyPath: \ForceUser.lightsaber.color]  // blue


// Access a property on the object returned by key path
let masterKeyPath = \ForceUser.master
let anakinMasterName = anakin[keyPath: masterKeyPath]?.name  // "Obi-Wan Kenobi"


// Change Anakin to the dark side using key path as a setter
anakin[keyPath: masterKeyPath] = sidiousan
akin.master?.name // Darth Sidious


// Note: not currently working, but works in some situations
// Append a key path to an existing path
//let masterNameKeyPath = masterKeyPath.appending(path: \ForceUser.name)
//anakin[keyPath: masterKeyPath] // "Darth Sidious"
스위프트의 키패스에서 장점은 강타입이라는 점이다! 더이상 Objective-C의 문자열 스타일로 지저분할 필요가 없다!

다열 문자열 리터럴(Multi-line String Literals)
많은 프로그래밍 언어에서는 일반적인 기능인 다열 문자열 리터럴이다. 스위프트4에서 이것을 추가했으며 세개의 쌍따옴표로 텍스트를 감싸는 형태의 문법이다 [SE-0168].

let star = "⭐️"let introString = """
  A long time ago in a galaxy far,
  far away....

  You could write multi-lined strings
  without "escaping" single quotes.

  The indentation of the closing quotes
      below deside where the text line
  begins.

  You can even dynamically add values
  from properties: \(star)
  """

print(introString) // prints the string exactly as written above with the value of star
XML/JSON을 만들떄나 UI에 보여줄 긴 양식의 텍스트를 만들때 매우 유용하다.

한쪽만의 범위(One-Sided Ranges)
장황한 표현을 줄이고 가독성을 개선하기위해 표준 라이브러리는 한쪽만 표현된 범위로 시작과 끝을 추론할 수 있다 [SE-0172].

한 인덱스에서 시작이나 끝 인덱스까지 범위의 컬랙션을 만들어보면 유용하다는 점을 바로 인지할 수 있다.
// Collection Subscript
var planets = ["Mercury", "Venus", "Earth", "Mars", "Jupiter", "Saturn", "Uranus", "Neptune"]
let outsideAsteroidBelt = planets[4...] // Before: planets[4..<planets.endIndex]

let firstThree = planets[..<4]          // Before: planets[planets.startIndex..<4]
보이는바와 같이, 한쪽만의 범위는 명시적으로 시작이나 끝 인덱스를 지정해줘야하는 필요를 줄여준다.

무한 시퀀스
시작이나 끝 인덱스가 셀 수 없는 타입일때 무한 Sequence를 정의할 도 있다.
// Infinite range: 1...infinity
var numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (8, "Neptune")]

planets.append("Pluto")
numberedPlanets = Array(zip(1..., planets))
print(numberedPlanets) // [(1, "Mercury"), (2, "Venus"), ..., (9, "Pluto")]

패턴 매칭
한쪽만의 범위를 사용할 때 또다른 좋은 사용 방법은 패턴 매칭이다.
// Pattern matching

func temperature(planetNumber: Int) {
  switch planetNumber {
  case ...2: // anything less than or equal to 2

   print("Too hot")
  case 4...: // anything greater than or equal to 4

   print("Too cold")
  default:
   print("Justtttt right")
 }
}

temperature(planetNumber: 3) // Earth

제네릭 서브스크립트
서브스크립트는 데이터 타입의 접근성을 직관적이게 만드는데 중요한 역할을 한다. 이런 유용함을 증진하기 위해 서브스크립트는 이제 제네릭으로 만들 수 있다 [SE-0148].

struct GenericDictionary<Key: Hashable, Value> {
  private var data: [Key: Value]

  init(data: [Key: Value]) {
   self.data = data
  }

  subscript<T>(key: Key) -> T? {
   return data[key] as? T
  }
}
이 예제에서는 리턴 타입이 제네릭이다. 그리하여 아래처럼 제네릭 서브스크립트를 사용할 수도 있다.
// Dictionary of type: [String: Any]
var earthData = GenericDictionary(data: ["name": "Earth", "population": 7500000000, "moons": 1])

// Automatically infers return type without "as? String"
let name: String? = earthData["name"]

// Automatically infers return type without "as? Int"
let population: Int? = earthData["population"]
리턴 타입을 제네릭으로 하는것 뿐만 아니라 실제 서브스크립트 타입을 제네릭으로 할 수도 있다.
extension GenericDictionary {
  subscript<Keys: Sequence>(keys: Keys) -> [Value] where Keys.Iterator.Element == Key {
   var values: [Value] = []
   for key in keys {
     if let value = data[key] {
       values.append(value)
     }
   }
   return values
  }
}

// Array subscript value
let nameAndMoons = earthData[["moons", "name"]]        // [1, "Earth"]

// Set subscript value
let nameAndMoons2 = earthData[Set(["moons", "name"])]  // [1, "Earth"]
이 예제에서는 두가지 다른 Sequence 타입(ArraySet)을 넣어 각각 값의 배열을 내뱉는다.

잡다한 것
여기까지는 스위프트4에서 큼직한 변화들을 다뤄봤다. 이제는 비교적 작은 부분들을 빠르게 볼 것이다.

MutableCollection.swapAt(_: _:)
MutableCollection은 이제 변경가능한 swapAt(_:_:) 메소드를 가진다. 이 메소드는 이름대로 동작하는데, 주어진 인덱스에 값을 스왑한다 [SE-0173].

// Very basic bubble sort with an in-place swap
func bubbleSort<T: Comparable>(_ array: [T]) -> [T] {
  var sortedArray = array
  for i in 0..<sortedArray.count - 1 {
   for j in 1..<sortedArray.count {
     if sortedArray[j-1] > sortedArray[j] {
       sortedArray.swapAt(j-1, j) // New MutableCollection method

     }
   }
  }
  return sortedArray
}

bubbleSort([4, 3, 2, 1, 0]) // [0, 1, 2, 3, 4]

연관 타입 제약(Associated Type Constraints)
이제 where을 사용하여 연관타입에 제약을 둘 수 있다 [SE-0142].

protocol MyProtocol {
  associatedtype Element
  associatedtype SubSequence : Sequence where SubSequence.Iterator.Element == Iterator.Element
}
프로토콜 제약을 사용하여 많은 associatedtype 선언이 그 값을 제약할 수 있다.

클래스와 프로토콜의 존재
결국 Objective-C에서 스위프트로 만들어진 이 기능은 한 타입이 클래스를 따를 수 있을 뿐만 아니라 여러 프로토콜도 따르면서 정의할 수 있는 기능이다 [SE-0156].

protocol MyProtocol { }
class View { }
class ViewSubclass: View, MyProtocol { }

class MyClass {
  var delegate: (View & MyProtocol)?
}

let myClass = MyClass()
//myClass.delegate = View() // error: cannot assign value of type 'View' to type '(View & MyProtocol)?'
myClass.delegate = ViewSubclass()

@objc 추론을 제한하기
스위프트 API를 Objective-C에서 쓸 수 있게 만들기 위해서는 @objc라는 컴파일러 속성을 사용한다. 대부분 스위프트 컴파일러는 여러분을 위해 추론해준다. 이 추론에서 커다란 세가지 이슈는 다음과 같다.
  1. 바이너리 크기를 상당하게 증가시킬 가능성을 가진다.
  2. @objc가 추론될 때 알고있는 것이 명확하지 않다.
  3. 무심결에 Objective-C selector 충돌을 만들어낼 가능성을 증가시킨다.
스위프트4는 @objc 추론을 제한하여 이 문제를 해결하였다 [SE-0160]. 이 말은 Objective-C의 모든 동적 디스패치 능력을 원할때는 명시적으로 @objc를 사용해야 함을 의미한다.

이런 변화를 만들어야하는 몇가지 예제는 다음과 같다. private 메소드, dynacmic 선언, NSObject 자식 클래스의 메소드.

NSNumber 연결하기
NSNumber와 스위프트의 넘버 사이에는 여러 답답한 동작들이 존재해왔는데, 이 언어에 너무 오랫동안 몰해왔다. 다행히 스위프트4는 이 버그를 잡았다 [SE-0170].

아래에는 그 동작들의 예제를 설명해준다.
let n = NSNumber(value: 999)
let v = n as? UInt8 // Swift 4: nil, Swift 3: 231
스위프트3에서 일어나는 이상한 이 동작은 넘버가 오버플로우되면 0에서 시작하게된다. 이 예제에선 999 % 2^8 = 231으로 나온다.

스위프트4는 숫자가 담겨진 타입안에 세이프하게 표현될 수 있을 때만 값을 반환하는 옵셔널으로 만들었다.

스위프트 패키지 매니저
지난 몇달동안 스위프트 패키지 매니저에 여러 업데이트가 있었는데, 가장 큰 변화는 다음과 같다.
  • 브랜치나 커밋 해시에서 의존성을 제공한다(Sourcing dependencies from a branch or commit hash)
  • 더 많은 수용할 수 있는 패키지 버전의 조작(More control of acceptable package versions)
  • 더 일반화하여 해결한 패턴으로 직관적이지 않은 핀닝 명령을 교체()
  • 컴펄레이션에 사용될 스위프트 버전 정의 기능(Ability to define the Swift version used for compilation)
  • 각 타겟마다 소스파일의 위치를 지정함(Specify the location of source files for each target)
이것들은 모두 SPM이 되어야할 모습의 큰 발자국이다.SPM은 아직 긴 여정이 남았지만 프로포절에 활발하게 남아서 윤곽을 잡는 일을 도와줄 수 있다.

최근에 다뤄지는 프로포절의 개괄적인 내용을 보려면 Swift 4 Package Manager Update을 확인해보자.

아직 진행중인 것
이 글을 쓰고 있는 시점에서 15가지 수용된 프로포절이 큐에 있다. 무엇이 어떻게 되가고 있는지 슬쩍 보고 싶으면 Swift Evolution Proposals에 들어가서 Accepted로 필터링해보자.

이제 모두 하나하나 따라가보는것 보단, Xcode9의 베타버전에서 업데이트 된 것을 포스팅 할 것이다.

여기서 어디로 까?
스위프트 언어는 지난 몇년간 매우 커지고 성숙해왔다. 프로포절 과정과 커뮤니티 관계는 이 파이프라인을 따라가면 쉽게 변경사항을 확인할 수 있게 만들어왔다. 또한 누구나 쉽게 직접 에볼루션에 영향을 줄 수 있게 해놓았다.쉽게

스위프트4에서 이런 변화들로, 우리는 마침내 ABI 안정성의 모퉁이에 위치하고 있다. 스위프트 버전을 업그레이드하는데 좀 덜 고통스러워지고 있다. 빌드 퍼포먼스와 툴의 기능은 광대하게 개선되고 있다. 애플의 생태계 바깥에서 사용하는 스위프트는 더욱 실용적이게 되어가고 있다. 그리고 생각하건데, 우리는 아마 오직 몇개만 직관적인 구현에서 벗어나는 String의 완전한 재작성일 것이다(And to think, we're probably only a few full rewrites of String away from an intuitive implementation). ;]

스위프트에 더 많은 것들이 들어왔다. 모든 변경사항들이 어떻게 되가는지 최신 정보를 유지하기 위해서, 아래 자료를 확인하면 된다.
스위프트4에대한 여러분의 생각은 어떤가? 여러분이 제일 좋아하는 변경사항은 무엇인가? 아직도 이 언어 바깥에서 미련이 있는 기능은 무엇인가? 여기서 다루지 않은 새롭고 멋진 무언가를 발견하였는가? 아래 주석에 알려주기 바란다!


이 블로그는 공부하고 공유하는 목적으로 운영되고 있습니다. 번역글에대한 피드백은 언제나 환영이며, 좋은글 추천도 함께 받고 있습니다. 피드백은 

으로 보내주시면 됩니다.



WRITTEN BY
tucan.dev
개인 iOS 개발, tucan9389

,