Swift를 사용해서 JSON을 다루는 방법을 배웠다.

Identifiable

Swift에서 Hashable 프로토콜을 준수하는 모든 타입의 인스턴스는 hashValue 라는 정수형 프로퍼티를 가집니다. 이 값은 개별 인스턴스를 식별하는 값으로 사용할 수 있습니다. 그렇게 때문에 해당 인스턴스의 고유함을 보장받아야 하는 자료구조는 Hashable을 준수해야 합니다(대표적으로 Set). 동일한 타입의 두 인스턴스가 같다면 두 인스턴스의 해시 값(hashValue)은 동일하지만, hashValue가 같다고 해서 두 인스턴스가 같은건 아닙니다(주의해야 합니다) 그리고 Hashbale 프로토콜은 Equatable 프로토콜을 지원해야 합니다. 따라서 때문에 Hashable은 Equatable을 충족시켜야 합니다. Swift는 struct와 enum에 관해선 언어 레벨에서 지원합니다. 당연히 사용자가 직접 정의할 수 있습니다.

그런데 Identifiable의 프로퍼티인 id는 Hashable 입니다. Identifiable, Hashable 그리고 Equatable은 무슨 차이가 있는걸까요?

@available(OSX 10.15, iOS 13, tvOS 13, watchOS 6, *)
public protocol Identifiable {

    /// A type representing the stable identity of the entity associated with `self`.
    associatedtype ID : Hashable

    /// The stable identity of the entity associated with `self`.
    var id: Self.ID { get }
}

public protocol Hashable : Equatable {

    /// The hash value.
    ///
    /// Hash values are not guaranteed to be equal across different executions of
    /// your program. Do not save hash values to use during a future execution.
    ///
    /// - Important: `hashValue` is deprecated as a `Hashable` requirement. To
    ///   conform to `Hashable`, implement the `hash(into:)` requirement instead.
    var hashValue: Int { get }

    /// Hashes the essential components of this value by feeding them into the
    /// given hasher.
    ///
    /// Implement this method to conform to the `Hashable` protocol. The
    /// components used for hashing must be the same as the components compared
    /// in your type's `==` operator implementation. Call `hasher.combine(_:)`
    /// with each of these components.
    ///
    /// - Important: Never call `finalize()` on `hasher`. Doing so may become a
    ///   compile-time error in the future.
    ///
    /// - Parameter hasher: The hasher to use when combining the components
    ///   of this instance.
    func hash(into hasher: inout Hasher)
}

Identifiable vs Hashable vs Equatable

Identifiable은 인스턴스의 고유 값으로 객체의 상태와 구분됩니다. Equatable이 객체의 상태를 기반으로 동등 비교를 진행한다면, Identifiable은 고유값을 기준으로 구분합니다. 아래 예를 통해서 확인해보세요.

struct Person: Identifiable {
    var id: String
    var name: String
    var age: String
}

extension Person: Equatable {}

var person = Person(id: "123456789012")
person.name = sd

person == person(id: "123456789012") // false
person.id == Parcel(id: "123456789012").id // true

Hashable의 hashValue는 객체가 변경 될 때 변경됩니다. hashValue는 임의로 생성된 Seed에 의해서 실행시 계산되므로 안정적이지 않습니다. hashValue는 실행시 값이 충돌 될 수 있으므로, 컬렉션에서 해당 값을 가져올 때 마다 동등성 검사를 수반해야 합니다(Equatable을 상속받는 이유죠).

그래서 이런 문제를 해결할 수 있는 Identifiable 잘 활용하시는게 좋습니다. 대충 어떻게 작성해야 될지 감이 안오면 아래 코드 처럼 UUID를 사용하는 코드를 스니핏으로 저장해 두시고 사용하세요.

struct Person: Identifiable {
    let id = UUID()
}

Codable with JSON

Swift 프레임워크에서 제공하는 JSON 관련 클래스는 Data, Codable, Encoder/Decoder로 구분 가능합니다.

Data

JSON을 사용하기 위해선 String이 아니라 Data 타입으로 변환해야 합니다. String에서 Data로 변환하는 것은 Swift에서 관련 메서드를 제공하기 때문에 힘들지 않습니다. String을 Data로 변환하는 이유는 문자열의 특성인 COW 때문입니다.

// NSString
open func data(using encoding: UInt, allowLossyConversion lossy: Bool) -> Data? // External representation

var jsonData = "Hello Swift".data(using: .utf8)

Codable

Codable은 개발자가 JSON 같은 데이터를 쉽게 encode 및 decode 할 수 있게 해주며, Encodable과 Decodable을 위한 프로토콜 입니다. 예를 들어, JSONEcoder를 사용해서 JSON으로 변환(encode)될 수 있는 타입은 Encodable을 준수해야 하며, JSONDecoder를 사용해서 JSON 데이터를 기반으로 인스턴스를 생성하려면 그 타입은 Decodable을 준수해야 합니다.

typealias Codable = Encodable & Decodable

Encodable

Codable(Encodable)을 준수한 타입만 JSON 으로 인코딩(Data -> JSON) 가능합니다.

struct Point: Codable {
  let x, y : Int
}
  
let origin = Point(x: 0, y: 0)

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

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

Decodable

Codable(Decodable)을 준수한 타입만 JSON을 객체로 변형 할 수 있습니다.

let jsonString = """
{
    "x" : 10,
    "y" : 10
}
"""

let jsonData = jsonString.data(using: .utf8)!

let decoder = JSONDecoder()

let point = try decoder.decode(Point.self, from: jsonData)
  
print(point)

CodingKeys

JSON 형태의 데이터로 변환하고자 하면, 기본적으로는 JSON 타입의 키(Key)와 사용자가 정의한 프로퍼티가 일치해야 합니다. 이때 Key와 프로퍼티의 이름을 다르게 사용하고 싶다면, CodingKeys라는 String 타입의 열거형을 선언하고 CodingKey 프로토콜을 준수하면 됩니다. 아래 를 참고하세요.

struct Landmark: Codable {
    var name: String
    var year: Int
    
    enum CodingKeys: String, CodingKey {
        case name = "title"
        case year = "date"        
    }
}

Ref