[Combine] Applying Sequence Operations to Elements - Operator 공부 6
안녕하세요 Pingu입니다.🐧
지난 글에서는 Combine의 Operator 중 Applying Matching Criteria to Elements로 분류된 Operator들을 알아봤었습니다. Upstrem에서 받은 값에 원하는 값이 있는지 혹은 조건에 만족하는 값인지를 확인해서 Bool 값을 Downstream으로 보내는 역할을 했습니다.
이번 글에서는 이어서 Applying Sequence Operations to Elements로 분류된 Operator에 대해서 알아보도록 하겠습니다.
Applying Sequence Operations to Elements
이번에 공부할 녀석들을 분류해둔 이름을 보면 값들에 Sequence 작업을 적용하는 역할을 할 거 같습니다. 그럼 먼저 여기에 분류된 Publisher에는 어떤 것들이 있는지부터 알아보겠습니다.
- DropUntilOutput
- Drop
- DropWhile
- TryDropWhile
- Concatenate
- PrefixWhile
- TryPrefixWhile
- PrefixUntilOutput
그리고 이 Publisher들을 활용해서 만든 Operator는 아래와 같습니다.
- drop(untilOutputFrom:)
- dropFirst(_:)
- drop(while:)
- append(_:)
- prepend(_:)
- prefix(_:)
- prefix(while:)
- tryPrefix(while:)
- prefix(untilOutputFrom:)
그럼 하나씩 어떤 역할들을 하는 것들인지 알아보도록 하겠습니다.
DropUntilOutput
첫 번째로 알아볼 Publisher는 DropUntilOutput 입니다.
정의를 보면 두 번째 Publisher로부터 값을 받을 때까지 Upstream Publisher의 값을 무시하는 Publisher라고 하네요. 이걸 활용해서 만든 Operator를 보면 이해가 빠르게 됩니다.
drop(untilOutputFrom:)
DropUntilOutput Publisher를 활용해서 만든 Operator는 drop(untilOutputFrom:)입니다.
정의를 보면 두 번째 Publisher에서 값을 받기 전에는 upstream Publisher에서 받는 값을 모두 무시한다고 되어있습니다.
여기서 두 번째 Publisher란 매개변수로 받은 Publisher를 의미합니다.
사용해보면 정말 쉽게 이해할 수 있어요
let upstreamPublisher = PassthroughSubject<Int, Never>()
let secondPublisher = PassthroughSubject<String, Never>()
upstreamPublisher
.drop(untilOutputFrom: secondPublisher)
.sink(receiveValue: { print($0)})
upstreamPublisher.send(1)
upstreamPublisher.send(2)
secondPublisher.send("a")
upstreamPublisher.send(3)
upstreamPublisher.send(4)
// drop(untilOutputFrom:) 예제 코드
3
4
위와 같이 두 번째 Publisher에서 값을 받기 전엔 Upstream Publisher에게 받은 값을 모두 무시하므로 결과에는 4, 5만 출력된 것을 볼 수 있습니다.
내부적인 작동방식을 조금 알아보자면, 두 번째 Publisher에게 값을 받으면 drop(untilOutputFrom:)은 두 번째 Publisher에 대한 subscribe를 취소한 뒤에 Upstream Publisher의 값을 받기 시작합니다.
Drop
다음으로 알아볼 Publisher는 Drop입니다.
정의를 보면 특정 개수의 값을 무시한 뒤부터 값을 내려보내는 Publisher라고 하네요.
그럼 바로 Drop을 활용해서 만든 dropFirst(_:)를 살펴볼게요.
dropFirst(_:)
dropFirst(_:)의 정의를 보면 특정 개수의 값을 무시한 뒤 부터 값을 내려보낸다고 되어있습니다.
바로 사용해보면 아래와 같이 간단하게 사용할 수 있습니다.
let intPublisher = [1, 2, 3, 4, 5].publisher
intPublisher
.dropFirst(3)
.sink(receiveValue: { print($0) })
// dropFirst(_:) 예제 코드
4
5
결과를 보면 3개까지는 무시하고 그 이후의 값들은 Downstream에 전달한 것을 볼 수 있습니다.
간단하죠?
DropWhile
다음으로 알아볼 Publisher는 DropWhile입니다.
정의를 보면 주어진 클로저가 false를 반환할 때까지 Upstream에서 받은 값을 무시하는 Publisher라네요.
얘도 사용해보면 이해가 빠를 거 같습니다.
drop(while:)
DropWhile을 활용해서 만든 Operator는 drop(while:)입니다.
정의를 보면 매개변수로 받은 클로저가 false를 반환할 때까지 Upstream publisher에서 받은 값을 무시한다고 되어있네요.
바로 사용해볼게요.
let intPublisher = [2, 4, 6, 1, 2, 3, 4, 5].publisher
intPublisher
.drop { value in
return value % 2 == 0
}
.sink(receiveValue: { print($0) })
// drop(while:) 예제 코드
1
2
3
4
5
위 코드를 보면 짝수이면 true를, 홀수이면 false를 반환하는 클로저를 사용했습니다.
결과를 보면 처음으로 받은 홀수 값인 1을 받은 이후부터 값을 Downstream으로 내려보낸 것을 알 수 있습니다.
이거도 사용해보니 아주 간단하게 이해할 수 있네요.
TryDropWhile
다음은 DropWhile에 Try가 붙은 TryDropWhile입니다.
얘는 뭐 딱 봐도 DropWhile이랑 동일한 역할인데 에러를 Downstream으로 보낼 수 있다는 차이점만 있어 보이네요.
실제로 정의를 읽어봐도 그렇습니다.
tryDrop(while:)
TryDropWhile을 활용해서 만든 Operator는 tryDrop(while:)입니다.
아까 알아본 drop(while:)에서 에러만 던질 수 있는 녀석이니 그냥 바로 사용해볼게요.
struct PinguError: Error { }
let intPublisher = [2, 4, 6, 1, 2, -1, 3, 4].publisher
intPublisher
.tryDrop { value in
guard value >= 0 else { throw PinguError() }
return value % 2 == 0
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// tryDrop(while:) 예제 코드
1
2
-1
3
4
finished
위 코드를 보면 Upstream에서 받은 값이 음수이면 에러를 던지게 만들었습니다.
엥 그런데 -1이 downstream으로 전달되었네요?
이런 결과가 나온 이유는 클로저에서 false가 나올 때까지만 클로저로 값을 처리하고 false가 나온 이후에는 그냥 어떤 값이든 내려보내기 때문입니다.
즉 false가 나오기 전에 에러를 발생시켜야 에러가 전달되는 거죠.
아래와 같이 값을 수정하면 에러를 발생시킬 수 있습니다.
struct PinguError: Error { }
let intPublisher = [2, 4, 6, -1, 1, 2, 3, 4].publisher
intPublisher
.tryDrop { value in
guard value >= 0 else { throw PinguError() }
return value % 2 == 0
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// tryDrop(while:) 예제 코드
failure(__lldb_expr_20.(unknown context at $10ec823bc).(unknown context at $10ec823c4).(unknown context at $10ec823cc).PinguError())
false가 발생하기 전에 에러를 발생하는 값을 받았기 때문에 에러가 발생되는 것을 볼 수 있습니다.
Concatenate
이번에는 Drop 시리즈가 아닌 Concatenate Publisher를 알아보겠습니다. Concatenate가 무슨 뜻인고 하니 연결하다는 뜻이 있네요. 정의를 보면 어떤 Publisher의 모든 값을 다른 Publisher의 값보다 먼저 내보내는 Publisher라고 되어있습니다.
이렇게만 보면 이해가 잘 안 되는데요, 이걸 활용해서 만든 Operator를 보면서 이해해보겠습니다.
append(_:)
Concatenate Publisher를 활용해서 만든 append(_:)를 알아보겠습니다.
정의를 보면 Upstream에서 받은 값들에 매개변수로 받은 값들을 추가해주는 역할을 한다고 되어있습니다.
Array에 append 메서드와 비슷한 역할을 하는 거 같네요.
바로 사용해볼게요.
let intPublisher = [1, 2, 3, 4].publisher
intPublisher
.append(5, 6)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// append(_:) 예제 코드
1
2
3
4
5
6
finished
결과를 보면 Upstream에서 값을 모두 받은 뒤에 매개변수로 받은 값을 내보내고 나서야 finished를 내려보내는 것을 볼 수 있습니다.
append(_:)의 매개변수에는 Sequence 타입도 사용할 수 있습니다.
let stringPublisher = ["My", "Name"].publisher
stringPublisher
.append(["Is", "Pingu"])
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// append(_:) 예제 코드
My
Name
Is
Pingu
finished
그리고 매개변수로 Publisher도 사용할 수 있습니다.
let firstPublisher = ["My", "Name"].publisher
let secondPublisher = ["Is", "Pingu"].publisher
firstPublisher
.append(secondPublisher)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// append(_:) using Publisher 예제 코드
My
Name
Is
Pingu
finished
prepend(_:)
다음으로 알아볼 녀석은 prepend(_:)입니다. 얘도 append(_:)와 동일하게 Concatenate Publisher를 활용해서 만든 Operator입니다.
정의를 보면 아까 append(_:)는 Upstream Publisher의 값을 먼저 내려보냈는데, prepend(_:)는 매개변수로 받은 값들을 먼저 내려보낸다고 합니다. 그럼 뭐 값의 순서만 바뀌고 나머지는 같을 거 같네요.
그리고 append(_:)와 마찬가지로 prepend(_:)도 매개변수로 값들과 Sequence, Publisher를 사용할 수 있습니다.
바로 3가지를 다 사용해볼게요.
let intPublisher = [1, 2, 3, 4].publisher
intPublisher
.prepend(5, 6)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// prepend(_:) using values 예제 코드
5
6
1
2
3
4
finished
let intPublisher = [1, 2, 3, 4].publisher
intPublisher
.prepend([5, 6])
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// prepend(_:) using Sequence 예제 코드
5
6
1
2
3
4
finished
let firstPublisher = [1, 2, 3, 4].publisher
let secondPublisher = [5, 6].publisher
firstPublisher
.prepend(secondPublisher)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// prepend(_:) using Publisher 예제 코드
5
6
1
2
3
4
finished
이렇게 모두 사용해보니 정말 간단하다는 것을 알 수 있네요.
PrefixWhile
다음으로 알아볼 Publisher는 PrefixWhile입니다.
보통 prefix라는 녀석들은 앞쪽의 몇 개만 처리하거나 하는 역할을 하니 비슷한 역할을 할 거 같네요.
정의를 보니 predicate 클로저를 만족할 때까지 값을 계속 내려보내는 Publisher라고 하네요.
이번에도 얘를 활용해서 만든 Operator들을 보며 이해해보겠습니다.
prefix(_:)
PrefixWhile Publisher를 활용해서 만든 Operator는 아니지만 비슷한 역할을 하는 Operator인 prefix(_:)부터 알아보겠습니다.
정의를 보면 매개변수로 받은 개수만큼만 Downstream으로 내려보내는 역할을 한다고 합니다.
간단하니 바로 사용해보겠습니다.
let intPublisher = [1, 2, 3, 4, 5].publisher
intPublisher
.prefix(3)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// prefix(_:) 예제 코드
1
2
3
finished
위 코드를 보면 3개까지만 내려보내라고 했더니 결과도 3개만 내려보낸 것을 볼 수 있습니다.
prefix(while:)
이번에는 PrefixWhile Publisher를 활용해서 만든 prefix(while:)입니다.
정의를 보면 매개변수로 받은 클로저가 false를 반환할 때까지 값을 내려보내는 역할을 한다고 합니다.
바로 사용해볼게요.
let intPublisher = [1, 2, 3, 4, 3, 2, 1].publisher
intPublisher
.prefix { value in
return value <= 3
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// prefix(while:) 예제 코드
1
2
3
finished
위 코드를 보면 클로저에서 false가 나올 때 까지는 값을 내려보내다가 false가 나오는 순간에 finished를 보낸 것을 볼 수 있습니다.
TryPrefixWhile
이번에 알아볼 녀석은 PrefixWhile의 Try 버전입니다.
PrefixWhile과 동일한 역할을 하면서 에러를 던질 수 있다는 차이점만 있습니다.
tryPrefix(while:)
TryPrefixWhile Publisher를 활용해서 만든 Operator는 tryPrefix(while:)입니다.
뭐 얘도 prefix(while:)과 동일한 역할을 하면서 에러만 던질 수 있겠네요.
바로 사용해보고 넘어가겠습니다.
struct PinguError : Error { }
let intPublisher = [1, 2, 3, 4, 3, 2, 1].publisher
intPublisher
.tryPrefix { value in
guard value <= 3 else { throw PinguError() }
return value <= 3
}
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
// tryPrefix(while:) 예제 코드
1
2
3
failure(__lldb_expr_11.(unknown context at $104ee169c).(unknown context at $104ee16a4).(unknown context at $104ee16ac).PinguError())
아까와 동일하게 3보다 작으면 true를 반환하는 클로저를 매개변수로 사용했습니다. 그리고 이번에는 3보다 큰 값을 받으면 에러를 던지게 만들어뒀어요.
결과를 보면 예상대로 4를 받았을 때 에러가 던져진 것을 볼 수 있습니다.
PrefixUntilOutput
이번 글의 마지막 Publisher는 PrefixUntilOutput입니다.
정의를 보면 Upstream이 아닌 다른 Publisher가 값을 내려보낼 때까지만 값을 내려보내는 Publisher라고 합니다.
구현부를 잠깐 보면...
public struct PrefixUntilOutput<Upstream, Other> : Publisher where Upstream : Publisher, Other : Publisher {
public typealias Output = Upstream.Output
public typealias Failure = Upstream.Failure
public let upstream: Upstream
public let other: Other
public init(upstream: Upstream, other: Other)
public func receive<S>(subscriber: S) where S : Subscriber, Upstream.Failure == S.Failure, Upstream.Output == S.Input
}
PrefixUntilOutput에는 upstream과 other라는 Publisher가 있습니다.
정의에서 말했듯 other라는 Publisher가 값을 내보낼 때까지만 Upstream의 값을 내려보내겠다는 뜻이겠네요.
prefix(untilOutputFrom:)
PrefixUntilOutput Publisher를 활용해서 만든 Operator는 prefix(untilOutputFrom:)입니다.
정의를 보면 매개변수로 받은 Publisher가 값을 내보낼 때까지만 값을 내려보낸다고 합니다.
한 번 사용해보면 이해가 빠르게 될 거 같아요.
let upstreamPublisher = PassthroughSubject<Int, Never>()
let otherPublisher = PassthroughSubject<String, Never>()
upstreamPublisher
.prefix(untilOutputFrom: otherPublisher)
.sink(receiveCompletion: { print($0) },
receiveValue: { print($0) })
upstreamPublisher.send(1)
upstreamPublisher.send(2)
otherPublisher.send("Pingu")
upstreamPublisher.send(3)
upstreamPublisher.send(4)
// prefix(untilOutputFrom:) 예제 코드
1
2
finished
위 코드를 보면 otherPublisher가 값을 내보낼 때까지만 upstreamPublisher의 값을 내려보내는 것을 볼 수 있습니다.
otherPublisher가 "Pingu"를 내려보낸 이후에는 upstreamPublisher가 finished 돼서 이후에 보낸 3, 4는 출력이 안 되는 것을 볼 수 있어요.
이렇게 Combine의 Publisher와 Operator 중에서 Applying Sequence Operations to Elements로 분류된 것들에 대해 알아봤습니다.
다음 글에서는 Selecting Specific Elements로 분류된 녀석들을 알아보도록 하겠습니다.
이번 글의 전체 코드는 여기에서 볼 수 있습니다.
감사합니다!