본문 바로가기
iOS/Combine

#3 Transforming Operators

by HaningYa 2020. 10. 28.
728x90

[출처:www.raywenderlich.com/books/combine-asynchronous-programming-with-swift/v2.0]

3장에서는 transforming operator에 대해서 배워봅니다.

Transforming operator 는 publisher 로 부터 받은 값을 subscriber 가 쓸 수 있는 형태로 변형하는 작업을 합니다.

Operators are publishers

콤바인에서 pulisher 로 부터 온 값에 operation을 수행하는 메서드를 Operators 라고 부릅니다.

각각의 operator는 publisher 를 반환합니다. 

다시 말하자면 Operator 는 publisher로 부터 upstream value 를 받아와 값을 변형하고 downstream 으로 데이터를 보냅니다.

이번장에서는 operator 를 사용하는 방법과 그것의 output 에 집중해서 배워볼 것 입니다.

operator가 error를 처리하는 용도로 쓰이지 않는 이상, upstream 으로 부터 error를 받는 다면 별 다른 동작없이 해당 error를 downstream 으로 publish 합니다.

* 이번장에서는 error handle 을 깊게 다루진 않고 16장에서 다룰 것 이다.

Collecting values

publishers 는 값을 하나씩 방출하거나 collections로 묶어서 값을 방출할 수도 있습니다.

list view를 다룰 때 등 과 같이 자주 collections 로 묶인 데이터를 사용하게 되는데 이떄 사용하는 operator 가 collect() 입니다.

collect()

collect operator 는 publisher 로 부터 방출된 개별값(individual)의 stream(흐름)을 묶어서 배열로 만들어 주는 작업을 수행합니다.

marble diagram

operator 가 어떻게 동작하는지는 앞으로 marble diagrams 를 통해서 설명합니다.

marble diagram 은 operator 가 어떻게 동작하는지 시각화 할 수 있습니다.

상단의 줄은 upstream publisher 이고, 구체적으로 subscriber가 operator를 통해 변형시킬 값들 입니다.

아랫줄은 downstream 으로 이것 또한 어떤 subscriber 가 operator 를 통해 변형시킬 값이 될 수도 있습니다.

그림에서 이해했듯이 collect() operator 는 개별 값들의 stream을 buffer 에 담아 upstream publisher 가 완료되었을 때 배열로 만들어 반환합니다.

예시코드를 보자면 

sink 를 통해 subscribe 하여 들어오는 값들을 출력합니다. 여기에 collect operator 를 적용하면

개별로 출력되던 값들을 buffer 에 모아서 completion이 되었을 떄 emit 하고 종료되는 것을 볼 수 있습니다.

*주의할 점은 collect는 buffer 에 저장할 최대 값이 무제한 입니다. 메모리를 필요한 대로 사용하기 때문에 주의해야 합니다.

collect operator 의 변형 버전들이 있는데 몇개씩 buffer 에 모아서 출력해줄지 정해줄 수 있습니다.

collect에 원하는 buffer 크기를 지정해 주면 해당 갯수만큼 모아서 반환해줍니다.

마지막 value E 같은 경우는 값이 하나인데도 배열로 리턴되었습니다. 왜냐하면 upstream publisher가 collect 의 buffer 가 채워지기 전에 완료되었기 떄문에 publisher 가 complete 되면 buffer 에 남은 것들을 배열로 반환합니다.

Mapping values

map(_:)

map operator 는 일반적인 Swift 의 map 과 비슷하지만 콤바인의 map은 publisher가 방출한 값에 대해서 사용한다는 점이 다릅니다.

marble diagram 을 보면 publisher 가 방출한 값에 2를 곱해서 downstream으로 emit 하는 걸 볼 수 있습니다.

하나씩 코드를 보자면

  1. 숫자를 글로 바꿔주는 number formatter를 생성합니다.
  2. 정수들을 emit 하는 publisher 를 만듭니다.
  3. map을 사용하여 upstream Publisher 가 emit 하는 값들을 받아서 formatter 를 사용하여 나온 숫자 글을 리턴하는 클로져를 전달합니다.

Map key paths

map operator 하나로 mapping, 두개로 mapping, 세개로 mapping 할 수 있는 가지 버젼이 있습니다. 

  • map<T>(_:)
  • map<T0, T1>(_:_:)
  • map<T0, T1, T2>(_:_:_:)

T는 주어진 key paths에서 발견되는 값의 타입을 뜻합니다.

다음 예제에서 Coordinate type을 사용하여 quadrantOf(x:y:) 메소드를 사용해 봅니다.

Coordinate type은 x와 y.quadrantOf(x:y:) 프로퍼티를 가지고 있고 x값과 y값을 파라미터로 받아와 x와 y가 있는 사분면을 문자열로 반환합니다.

example(of: "map key paths") {
  // 1
  let publisher = PassthroughSubject<Coordinate, Never>()
// 2
publisher
// 3
.map(\.x, \.y) .sink(receiveValue: { x, y in
// 4
      print(
        "The coordinate at (\(x), \(y)) is in quadrant",
        quadrantOf(x: x, y: y)
      )
})
.store(in: &subscriptions)
// 5
publisher.send(Coordinate(x: 10, y: -8))
publisher.send(Coordinate(x: 0, y: 5)) }

코드를 보자면

  1. Coordinates 를 방출하는 publisher 를 만듭니다. (Never 에러 타입)
  2. publisher 에 subscription 합니다.
  3. Coordinate 의 x와 y값을 그들의 keypath를 사용하며 mapping 합니다.
  4. x값과 y값이 몇사분면에 있는지 메소드의 결과를 출력합니다.
  5. publisher 에게 Coordinate 값을 전달해 emit 하게 합니다.
 ——— Example of: map key paths ———
The coordinate at (10, -8) is in quadrant 4
The coordinate at (0, 5) is in quadrant boundary

tryMap(_:)

map 을 포함한 몇가지 operator 들은 상응하는 try operator 가 있습니다.

try operator 를 통해 error 를 throw 할 수 있는 closure 를 받을 수 있습니다.

  1. 존재하지 않는 디렉토리 이름을 표현하는 string을 방출하는 publisher를 만듭니다.
  2. tryMap을 사용하여 존재하지 않는 디렉토리의 컨텐츠를 가져오도록 시도합니다.
  3. 값이나 completion event 를 출력해봅니다.

클로져 내에서 try 키워드를 사용한 것을 볼 수 있습니다.

실행시켜 보면 tryMap은 failure complition event 와 함께 위에서 작성한 값을 출력합니다.

Flattening publishers

flatMap(maxPublishers:_:)

flatMap operator는 다수의 upstream publisher 하나의 downstream publisher로 flatten 하는데 쓰일 수 있습니다.

다시말해 다수의 publisher들이 방출한 것들을 편평하게 만드는 작업을 합니다.

flatMap 이 return 하는 publisher 는 아마도 upstream publisher에게 받은 type 과 일치하지 않을 가능성이 큽니다.

flatMap이 어떻게 다수의 publisher 입력값을 flatten 하는걸 이해하기 전에 먼저 flatMap 의 output 부터 보겠습니다.

flatMap 이 자주 사용되는 케이스는 

when you want to subscribe to properties of values emitted by a publisher that are themselves publishers.

그들 자체가 publisher 인 publisher의 프로퍼티를 subscribe 하고 싶을때 사용한다.

Chatter 구조체는 두개의 프로퍼티가 있다.

name 은 일반적인 문자열이고 message는 CurrentValueSubject subject로 message string을 받아서 초기화 된다.

  1. charlotte 과 james라는 두개의 Chatter instance 를 만든다.
  2. charotte 로 초기화된 chat publisher 를 만든다.
  3. chat publisher 를 구독해서 Chatter 구조체에서 받은 message를 출력한다.

CurrentValueSubject 이기 떄문에 subscribe 하면 현재 값(초기화 값)을 출력한다.

이제 코드를 더 추가해보자

  1. charlotte의 currentValueSubject인 message 값을 바꿨다.
  2. chat의 현재값을 james 로 바꿨다.

실행시켜보면 Charlotte의 새로운 메시지는 볼 수 없고 james의 초기 메시지만 볼 수 있다.

왜냐하면 우리는 Chatter를 publish 하는 chat publisher에 구독을 한 것이지
emitted 된 Chatter 구조체 내의 message 라는 publisher에 구독하지 않았기 떄문이다.

만약에 모든 chat 에 대한 message 를 출력하고 싶을 때 flatMap을 사용한다.

  • Chatter 구조체($0)의 message를 flatten 한다.
  • 받은 값 handler도 수정한다. flatten 한 결과로 string 이 나오기 때문에 Chatter.message.value 가 아닌 string 을 바로 출력한다.

그리고 메시지를 더 넣어서 출력해보면 모든 메시지가 출력된다.

정리하자면 Struct (publisher) -> Struct.message (Publisher) 구조에서 Struct 에 publisher 를 달고 flatMap operator를 사용해 message를 subscribe 할 수 있었다.

flatMap의 정의를 다시 생각해 보면 flatMap 이란

flatMap flattens the output from all received publishers into single publisher

이 뜻은 collect() Operator 처럼 메모리 이슈가 발생할 수 있다는 것 이다.

왜냐하면 flatMap은 operator 가 downstream 으로 반환할 single publisher를 update 하기 위해 우리가 send 하는 publisher들을 전부 buffer 에 담아두기 때문입니다.

해결하기 위해 선택적으로 얼마나 많은 publisher들을 buffer 에 담을지 maxPublishers파라미터를 통해 지정할 수 있습니다.

따로 이 값을 정하지 않으면 디폴트 값으려 .unlimited가 들어갑니다.

marble diagram 을 보면 flatMap은 현재 3개의 publisher를 수신합니다. 

각각의 publisher 는 value 라는 publisher를 프로퍼티로 가지고 있습니다.

flatMap 은 publisher P1, P2에서 emit 된 값을 downstream 으로 전달하지만 P3은 전달되지 않습니다.

왜냐하면 maxPublisher 에 최대 2개의 publisher 만 저장하도록 설정했기 때문입니다.

Chatter 구조체를 하나 더 만들어 실제로 이렇게 동작하는지 확인해보겠습니다.

  1. 세번째 Chatter instance 를 만듭니다
  2. chatter instance 를 chat publisher 에 추가합니다.
  3. charlotte 의 메시지를 수정합니다.

flatMap 이 최대 2개의 publisher 를 가질 수 있기 때문에 Morgan 의 메시지는 출력되지 않는 것을 볼 수 있습니다. 

flatMap은 input 다른 타입의 output 으로 만드는 유일한 operator가 아니기 때문에 다른 몇가지 operator 를 알아보고 이 장을 끝내겠습니다.

Replacing upstream output

map 예제 이전에 Formatter.string(for:) 메소드를 사용했었습니다. 이 메소드는 optional string을 리턴하는데 이전에는 ?? operator 를 통해 nil 값을 non-nil 값으로 바꿔줬습니다.

Combine 역시 항상 값을 전달할 수 있는 (non-nil) operator 가 있습니다.

replaceNil(with:)

예제 코드를 입력합니다.

  1. optional string array로 부터 publisher 를 생성합니다.
  2. replaceNil(with:) 을 사용하여 upstream publisher 로부터 받은 nil값을 전부 새로운 Non-nil 값으로 바꿉니다.
  3. 값을 출력합니다.

optional values는 non-optional 로 바뀌진 않았습니다.

함수의 이름 그대로 nil을 Non-nil 로 바꿔주는 역할이지 non-optional 로 unwrapping 까지 해주진 않습니다.

unwrapping하기위해 map 을 사용해봅니다.

여기서 replaceNil을 사용할지 ?? 를 사용해야되는지 고민이 듭니다.

?? operator 는 또다른 optional 를 반환할 수 있지만 replcaeNil은 그렇지 못합니다. 

확인해보기 위해 replaceNil의 반환타입을 String?으로 해보면 

에러가 난다.

codershigh.dscloud.biz:30004/t/swift-coalescing-operator/285


이 문장이 이해가 어려웠다.

There is a subtle but important difference between using ?? and replaceNil.
The
?? operator can return another optional, while replaceNil cannot.

replaceNil은 optional 타입을 받아서 nil 인 경우 다른 값으로 바꿔서 downstream으로 전달한다.
이 때 nil 대신 바꾸는 값은 optional type 이 되면 안된다.

그런데 replaceNil 이 downstream으로 보내는 값들은 optional type이다. (map 으로 unwrapping 해야되기 떄문에)

그럼 ?? (coalescing operator) 는 optional 을 쓸 수 있다는데 

아 이렇게 ??  다음 optional 을 전달할 수 있다는 것 이다.

?? 에 대한 이해가 부족해서 저문장을 이해하기 어려웠던 것 같다.

nil 을 map operator 에서 B로 바꿔주는데 B 또한 String optional 이다. 그래서 sink 에서 값을 받았을 때 unwrapping 하라고 warning 이 뜬다.

그냥 말 그대로 replaceNil 에서 nil을 바꿔주는 값은 optional 이 아니여야 하고 ?? 는 nil 대신 바꿔주는 값 또한 optional 값으로 바꾸줄 수 있다는 소린데
이해가 안되는건 왜 optional 값을 을 다루는 ?? operator 가 optional 값을 대신 넣어줄 수 있게 만들었는지 모르겠다.


replaceEmtpy(with:)

replaceEmty(with:) operator를 사용해서 publisher 가 아무 값을 방출하지 않고 complete 될 때 값을 replace 하거나 insert 할 수 있다.

아래 그림을 보면 Publisher 가 아무 값을 방출하지 않고 끝나는걸 볼 수 있다. 그리고 끝나는 시점에 replaceEmpty(with:) operator 가 downstream 으로 값을 넣어준다.

코드로 확인해보자

replaceEmty(with:) 없이는 아무런 방출된 값이 없을 때 별일없이 complete 된다.

  1. 바로 completion event 를 전달하는 publisher를 만든다.

sink 이전에 replaceEmty()를 사용하게 되면 complete 되기 이전에 1을 emit 하고 끝이난다.

Incrementally transforming output

고차함수와 비슷하게 동작하는 Combine operator들을 봤다면 이번 챕터에서는 조금 다른 operator 들을 배워봅니다.

scan(_:_:)

scan operator는 upstream publisher 가 방출한 현재 값을 클로져에게 전달함과 동시에 해당 클로져가 반환한 마지막 값 또한 포함해서 전달합니다.

marble diagram 을 보자면 scan 은 0을 초기값으로 시작하여 publisher 에게 값을 받으면 이전값과 받은 값을 더해서 결과값을 방출합니다.

예제 코드를 보자면

  1. -10 과 10 사이 랜덤 정수를 만드는 computed property 를 만듭니다.
  2. 이 computed property를 사용해 0~21번 랜덤한 값을 방출하는 publisher를 만듭니다.
  3. 50으로 시작하여 scan 을 시작합니다. max를 사용하여 결과값이 음수가 되지 않게 합니다.

error-throwing closure 를 사용가능한 tryScan operator 도 있다.

다른점은 tryScan 은 실패했을 경우 error 와 함께 complete 된다는 점이다.

Challenge

create a phone number lookup using transforming operators

2가지 일을 하는 publisher를 만드는 것 이다.

  1. 10자리의 숫자나 글자 문자를 입력받는다.
  2. 주소록에서 그 숫자를 찾아본다.

이 챌린지를 스텝바이스텝으로 나눠보면

  1. input 을 정수로 바꾼다. convert 함수를 사용해라
    (정수 타입으로 바꾸지 못할 경우 nil을 리턴한다.)
  2. nil 이 반환되면 0으로 replace 한다.
  3. 한번에 10개의 숫자를 collect 한다 (3 지역번호, 7 핸드폰번호)
  4. collected string을 format 하여 contracts dictionary 와 match 할 수 있도록 한다.
    주어진 format 함수를 사용해라
  5. 이전 operator 가 반환한 값을 dial 함수를 적용해라

solution

input
	.map(convert)
    .replaceNil(with: 0)
    .collect(10)
    .map(format)
    .map(dial)
    .sink(receiveValue: { print($0) } )

 

Key point

  • publisher 로 부터 출력된 값에 대해 operation을 수행하는 메서드를 operator 라고 부른다.
  • operator 는 publisher 이다.
  • transforming operator 는 upstream publisher로부터 input을 받고 downstream operator 가 사용할 수 있도록 output 을 변환해 준다.
  • Marble diagram 은 combine operator 가 어떻게 동작하는지 시작화 할 수 있는 좋은 방법이다.
  • 값을 buffering 하는 collect 나 flatMap 같은 operator 를 사용할 때 메모리 문제를 피할 수 있게 주의해라
  • swift 일반 operator 의 몇몇은 combine operator와 이름은 비슷하지만 하는 역할이 다를 수 있기 때문에 주의해라
  • 다수의 operator 는 subscription에 같이 chaining 될 수 있다.
728x90

'iOS > Combine' 카테고리의 다른 글

#12 Key-Value Observing  (0) 2020.10.30
#9 Networking  (0) 2020.10.28
"UI events are asynchronous"  (0) 2020.10.28
#2 Publishers & Subscribers  (0) 2020.10.27
#1 Hello, Combine  (0) 2020.10.27

댓글