관리 메뉴

김종권의 iOS 앱 개발 알아가기

[iOS - Swift] map, flatMap 이해하기, 직접 구현 방법 (+ json to model, json to dictionary) 본문

iOS 응용 (swift)

[iOS - Swift] map, flatMap 이해하기, 직접 구현 방법 (+ json to model, json to dictionary)

jake-kim 2022. 9. 6. 22:37

Swift에서의 map의 역할

  • map, flatMap 둘 다 클로저가 Optional일 때와 non-Optional일때의 기능이 다르므로 주의
    • Optional일때의 기능 - 형변환 (flatMap은 형변환을 완료하고 unwrapping까지 수행)
    • non-Optional일때의 기능 - 원소들에 하나하나씩 접근하여 변형을 주는 것
  • non-Optional일때의 map 예시)
    let someString = "123456"
    let newString = someString
      .map { String($0) + "a" }
    print(newString) // ["1a", "2a", "3a", "4a", "5a", "6a"]
  • non-Optional일때의 flatMap 예시)
    • flatMap의 역할 - Sequence의 배열에 변형을 주는 역할 (flat - 납작하게 차원을 줄이는 역할)
    • map은 각 요소(element)에 변형을 주지만, flatmap은 element에 접근하지 않고 현재 차원에만 변형을 줌
    • map은 각 요소(element)에 변형을 주어 차원을 늘리지만, flatMap은 차원을 좁히는 역할
// ex1
let someString = "123456"
print(someString.map { String($0) + "a" }) // ["1a", "2a", "3a", "4a", "5a", "6a"]
print(someString.flatMap { String($0) + "a" }) // ["1", "a", "2", "a", "3", "a", "4", "a", "5", "a", "6", "a"]

// ex2
let arr = [[1,2,3], [3,4,5]]
print(arr.map { $0 + [0] }) // [[1, 2, 3, 0], [3, 4, 5, 0]]
print(arr.flatMap { $0 + [0] }) // [1, 2, 3, 0, 3, 4, 5, 0]

ex) 직접 map처럼 만든 메소드

  • closure를 받아서, 해당 closure에 원래의 element값들을 하나씩 넣고 새로 생겨난 배열을 리턴
extension Sequence {  
  @inlinable public func myMap<T>(_ transform: (Element) -> T) -> [T] {
    var result = [T]()
    for e in self {
      result.append(transform(e))
    }
    return result
  }
}

ex) 직접 flatMap처럼 만든 메소드

  • @inlinable로 코드 최적화
  • 외부에서 클로저를 받으면 그 클로저를 element하나씩 수행하면서 새로운 배열에 담아서 리턴
  • rethrows 키워드를 사용한 이유는, myFlatMap 함수에서 인자로 throws 클로저를 받는데, 이 클로저에서 예외처리를 myFlatMap에서 하지 않고 myFlatMap을 호출하는 쪽에서 직접 처리하도록 예외를 던지기 위함 (rethows 관련 글 참고)
extension Sequence {    
  @inlinable public func myFlatMap<T: Sequence>(
    _ transform: (Element) throws -> T
  ) rethrows -> [T.Element] {
    var result = [T.Element]()
    for e in self {
      result.append(contentsOf: try transform(e))
    }
    return result
  }
}
  • Optional일때의 map과 flatMap) 
    • Swift에서의 map, flatMap은 Sequence의 extension으로도 존재하지만, Optional의 extension으로도 존재
    • Optional인 값을 map을 이용하면 차원을 늘리는게 아닌, 값의 변형이 가능
// swift에서 map, flatMap 선언부
@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
  case none
  case some(Wrapped)
  
  public init(_ some: Wrapped)
  
  @inlinable public func map<U>(_ transform: (Wrapped) throws -> U) rethrows -> U?
  @inlinable public func flatMap<U>(_ transform: (Wrapped) throws -> U?) rethrows -> U?
}

ex) Optional일 경우 > 형변환

let someString3: String? = "123"
print(someString3.map(Int.init)) // Optional(Optional(123))

 

map을 이용하여 decoding해보면?

우선 map없이 decoding하는 방법에 대해 짧게 알아보면,

  • decoding, encoding 용어
    • decoding: 바이너리 파일(Data)을 model타입으로 변경하는 것
    • encoding: model타입을 바이너리 파일(Data)로 변경하는 것
  • swift에서 jsonString을 model이나 Dictionary형태로 변경하는 방법?
    • jsonString > Data(utf8) > 원하는 형태로 변경
  • jsonString > 모델 형태 변경
struct MyModel: Codable {
  let type: String
  let id: String
}

let jsonString: String = """
{
   "type":"someType",
   "id":"someID"
}
"""

// String -> Model
/// jsonString > Data(utf8) > model
let modelNormal = try? JSONDecoder().decode(MyModel.self, from: Data(jsonString.utf8))
print(modelNormal) // MyModel(type: "someType", id: "someID")
  • jsonString > [String: Any] 형태 변경
// String -> [String: Any]
/// jsonString > Data(utf8) > [String: Any]
let dictNormal = try? JSONSerialization.jsonObject(with: Data(jsonString.utf8), options: []) as? [String:Any]
print(dictNormal) // ["id": someID, "type": someType]

Swift에서 map을 이용하여 디코딩 아이디어

  • moya처럼 사용하여 디코딩되게끔하면 더욱 선언적이고 깔끔한 코드가 될 수 있지 않을까?
    • * Moya의 map - RxMoya에서는 map을 디코딩할때 사용

https://github.com/Moya/Moya/blob/master/docs/Examples/Response.md

map을 이용하여 디코딩 방법

  • Sequence에 확장된 map 함수가 아닌, Optional에 확장된 map, flatMap함수를 사용하면 적절히 형변환이 가능
  • jsonString타입이 Optional이면 형변환처럼 동작할테니 이점을 활용하여 decoding

ex) jsonString을 Model로 형변환

  • map을 사용하면 선언형 프로그래밍이 되고, 더욱 이해하기 쉬운 코드로 표출
let jsonString: String? = """
{
"type":"someType",
"id":"someID"
}
"""

/// jsonString > Data(utf8) > model
let model = jsonString
  .map(\.utf8)
  .map(Data.init(_:))
  .flatMap { try? JSONDecoder().decode(MyModel.self, from: $0) }

// Optional(MyModel(type: "someType", id: "someID"))

ex) jsonString을 [String: Any]로 형변환

let dict = jsonString
  .map(\.utf8)
  .map(Data.init(_:))
  .flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] }
  
// Optional(["type": someType, "id": someID])

cf) 마지막에 flatMap이 아닌 map을 써도 되지만, 인수로 들어가는 transform이 Optional일때의 map은 unwrapping을 해주지 않고, flatMap은 wrapping까지 해주는 점 때문에 flatMap을 사용

// flatMap대신 map을 쓴 경우: unwrapping되지 않음

let dict = jsonString
  .map(\.utf8)
  .map(Data.init(_:))
  .map { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] }
  
// Optional(Optional(["type": someType, "id": someID]))

 

* 전체 코드: https://github.com/JK0369/ExMapFlatMap

 

* 참고

https://github.com/Moya/Moya/blob/master/docs/Examples/Response.md

Comments