2025. 3. 25. 19:55ㆍFramework, Library/Combine
오늘 글은 세 번째 Combine Operator인 Filtering Operators에 대해 알아보고자 합니다!
Filtering Operator는 Upstream Publisher가 방출하는 값을 필터링해, 필요한 데이터만 처리할 수 있도록 돕는 연산자입니다.
한 개 이상의 Publisher 값들을 서로 연결했던 Combining Operator,
Publisher의 값을 변형했던 Transforming Operator에 이어서 세 번째로 Publisher의 값을 걸러주는 연산자는 어떤 것들이 있는지 차근차근 알아보도록 하죠.
Filter, TryFilter
✔️ filter : 조건을 만족하는 값만 Publish함.
✔️ tryFilter : filter와 유사. 단, 조건을 평가하는 과정에서 에러를 Publish할 수 있음.

filter와 tryFilter는 모두 Upstream에서 방출되는 데이터를 걸러낼 수 있는 연산자입니다.
유일한 차이점이 있다면,
filter는 조건에 맞는 값만 골라내주는 역할이고 / tryFilter는 조건에 따라 에러를 던질 수 있게 된다는 점?
매우 간단하고 명확합니다. 긴 설명 필요없이 코드로만 살펴보면 될 것 같아요.
var publisher = PassthroughSubject<Int, Never>()
var cancelBag = Set<AnyCancellable>()
publisher
.filter {
$0.isMultiple(of: 4)
}
.sink { result in
print("\(result)은 4의 배수입니다.")
}
.store(in: &cancelBag)
[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach {
publisher.send($0)
}
// 4은 4의 배수입니다.
// 8은 4의 배수입니다.
publisher
.tryFilter {
guard $0 % 5 == 0 else { throw ErrorType.anyError }
return true
}
.sink(receiveCompletion: {
print($0)
}, receiveValue: { value in
print(value)
})
.store(in: &cancelBag)
(5...10).forEach {
publisher.send($0)
}
// 5
// failure(ErrorType.anyError)
removeDuplicates
✔️ removeDuplicates : 연속적으로 동일한 값이 Publish된 경우, 중복 값을 제거하고 Publish함.

아래 코드를 보고 바로 설명을 해볼게요.
현재 Upstream Publisher에서는 순서대로 0, 4, 4, 4, 4, 8, 8, 8, 12, 12, 12의 Int 값을 내보내고 있습니다.
Downstream sink 부분에서는 방출된 값을 바탕으로 "\(number)은 새로운 값입니다." 라는 문장을 print하라고 작성되어 있군요.
이 상황에서 실제 print 로그를 살펴보게 되면, 방출되는 모든 값이 전부 찍히는 것이 아니라 / 0, 4, 8, 12 값 한 번씩만 출력된 것을 확인할 수 있죠.
이게 바로 removeDuplicates 연산자가 갖는 역할입니다.
중복된 Publish 값을 모두 지워주고, 최초 새로운 값만 방출시키는 Operator가 바로 removeDuplicates입니다.
publisher
.filter {
$0.isMultiple(of: 4)
}
.removeDuplicates()
.sink { result in
print("\(result)은 새로운 값입니다.")
}
.store(in: &cancelBag)
[0, 4, 4, 4, 4, 8, 8, 8, 12, 12, 12].forEach {
publisher.send($0)
}
// 0은 새로운 값입니다.
// 4은 새로운 값입니다.
// 8은 새로운 값입니다.
// 12은 새로운 값입니다.
추가로, removeDuplicates 파라미터 by를 사용해서 요소의 동일성에 대한 추가 판단 조건을 클로저로 정의할 수도 있습니다.
만약 값이 하나가 아니라 두 개 이상을 담고 있는 아래와 같은 경우일 때는,
$0 (이전 값)과 $1 (현재 값)을 구체적으로 비교할 수도 있을 겁니다.
String의 특정 글자 부분을 가지고 비교하거나, 시간의 일부가 같을 경우에 Publish를 제한하고 싶을 때 파라미터를 함께 활용할 수 있을 것 같군요 !
publisher
.removeDuplicates(by: {
return $0.0 == $1.0 && $0.1 == $1.1
})
.sink { result in
print("\(result)은 새로운 요소입니다.")
}
.store(in: &cancelBag)
[(1, 3), (1, 3), (2, 1), (2, 2), (2, 2)].forEach {
publisher.send($0)
}
// (1, 3)은 새로운 요소입니다.
// (2, 1)은 새로운 요소입니다.
// (2, 2)은 새로운 요소입니다.
First, Last
✔️ first : 조건에 일치하는 첫 번째 값만 Publish함.
✔️ last : 조건에 일치하는 가장 마지막 값만 Publish함.

자세하게 설명할 것도 없이 매우 명확합니다.
first는 데이터 스트림 중 첫 번째 값을 방출하고 완료되는 연산자 / last는 데이터 스트림 중 마지막 값을 방출하고 완료되는 연산자입니다.
또한 first와 last 모두 파라미터 where 절을 통해 발행되는 특정한 조건을 추가할 수도 있습니다.
단! first와는 다르게 last에는 ⚠️주의⚠️해야 할 점이 하나 있어요!
바로 last는 completion 이벤트가 발생될 때까지 기다렸다가 데이터 스트림이 종료된 후 마지막 값을 방출한다는 점인데요.
만약, Publisher가 아무런 값을 방출하지 않고 완료되거나, 데이터 스트림이 종료되지 않는 Publisher인 경우에는 조건에 일치하는 값을 찾을 수 없어 아무런 값이 발행되지 않을 수 있습니다.
예시를 들어보죠!
아래 코드에서는 아무런 값이 방출되지 않을 겁니다. 그 이유가 무엇일지 스크롤 하기 전에 잠깐 생각해 볼까요?
var publisher = PassthroughSubject<Int, Never>()
var cancelBag = Set<AnyCancellable>()
publisher
.filter{
$0.isMultiple(of: 4)
}
.last()
.sink { result in
print("\(result)은 4의 배수입니다.")
}
.store(in: &cancelBag)
[4, 4, 4, 4, 8, 8, 8, 12, 12, 12].forEach {
publisher.send($0)
}
// 아무런 값도 출력되지 않음
PassthroughSubject나 CurrentValueSubject는 구독권을 생성할 때, 값을 무제한으로 요청 request(.unlimited) 하고 데이터를 수동으로 발행한다는 특징이 있는데요.
아래 코드는 데이터 스트림이 명시적으로 종료된다는 코드가 없어, last Operator에서 조건에 일치하는 마지막 값을 찾을 수 없는 것이죠.
쉽게 말해, completion 이벤트가 발생하지 않는다는 것입니다!
var publisher = PassthroughSubject<Int, Never>()
var cancelBag = Set<AnyCancellable>()
publisher
.filter{
$0.isMultiple(of: 4)
}
.last()
.sink { result in
print("\(result)은 4의 배수입니다.")
}
.store(in: &cancelBag)
[4, 4, 4, 4, 8, 8, 8, 12, 12, 16].forEach {
publisher.send($0)
}
publisher.send(completion: .finished)
// 16은 4의 배수입니다.
이런 상황에서는 Subject의 completion 이벤트를 명시적으로 호출해 Subject의 종료를 알려주는 방식으로 개선할 수 있습니다.
마지막 Publish에 해당하는 16이 포함된 값이 last에 의해 최종 방출될 거에요!
last Operator를 사용할 때는 Subject 사용 시에 특히 주의하면 좋겠죠? 😊
drop
✔️ dropFirst : 몇 개의 값을 건너뛰고 Downstream에 Publish함,
✔️ drop(while:) : 주어진 조건이 참인 동안 값을 건너뛰고, 처음 거짓이 된 이후 값부터 Publish함.

다음 볼 Operator는 Drop과 관련된 Combine Operator입니다.
컴바인에서 Drop은 특정조건에 따라 카운트 값을 건너뛰는 데 사용되는 연산자이고,
일정 카운트를 지정해서 건너뛰고 싶은 경우에는 dropFirst를 / 일정 조건이 충족되는 동안 건너뛰고 싶은 경우에는 drop(while:)을 사용합니다.
특히 drop(while:)문에서 주의할 점은
조건이 false가 될 때만 방출하는 것이 아니라, 조건이 처음으로 false가 되면, 그 이후의 값은 조건과 상관없이 모두 Publish 된다는 점입니다!
나머지는 그렇게 어렵지 않고, 위 이미지에서 표현한 게 전부니 가볍게 넘어갈게요 :)
prefix
✔️ prefix : Upstream에서 방출하는 값 중 처음 N개만 Downstream에 Publish함.

위에서 dropFirst를 살펴봤지만, prefix는 이와 정확하게 반대의 역할을 합니다.
파라미터로 지정하는 퍼블리셔의 처음 카운트 값만큼만 데이터 스트림을 작동시키고 / 카운트 값 이상은 방출하지 않습니다.
(1...10).publisher
.prefix(4)
.sink { print($0) }
.store(in: &cancelBag)
// 1 2 3 4
이와 같은 기본 사항에서 더욱 발전시켜,
while 파라미터를 사용해서 조건을 설정할 수도 / untilOutputFrom 파라미터를 사용해서 다른 Publisher의 값 방출 시점을 기다릴 수도 있습니다.
복잡하게 생각할 필요없이 가볍게 받아들이면 좋을 것 같아요!
ignoreOutput
✔️ ignoreOutput : Upstream에서 방출하는 모든 값을 무시하고, 최종 완료 이벤트만 전달합니다.

아래 코드에서는 1부터 10000까지의 값을 Publish하고 있지만,
ignoreOutput 연산자에 의해 Upstream의 값 하나하나가 방출되지 않고 / 오직 값과 상관없는 최종 완료 이벤트만 방출됩니다.
즉, 아래 코드에서는 "finished되었습니다." 문장만 출력되겠죠! 이게 전부입니다.
(1...10000).publisher
.ignoreOutput()
.sink(receiveCompletion: {
print("\($0)되었습니다.")
}, receiveValue: {
print("\($0)를 받았습니다")
})
.store(in: &cancelBag)
// finished되었습니다.
replaceEmpty, replaceError, replaceNil
✔️ replaceEmpty : Upstream에서 방출되는 값이 Empty일 경우 지정된 값으로 대체하여 Publish함.
✔️ replaceError : Upstream에서 방출되는 값이 Error일 경우 지정된 값으로 대체하여 Publish함.
✔️ replaceNil : Upstream에서 방출되는 값이 Nil일 경우 지정된 값으로 대체하여 Publish함.
마지막으로 값을 대체 (replace)하는 세 개의 연산자를 알아보겠습니다.
순서대로 Empty, Error, Nil 값에 대해 파라미터 with 부분에 정의되어 있는 값으로 대체하여 Downstream으로 Publish 할 수 있죠!
'Framework, Library > Combine' 카테고리의 다른 글
[Combine] Combine Operator 완전 정복하기 (4) - Timing and Control Operators (0) | 2025.04.06 |
---|---|
[Combine] Combine Operator 완전 정복하기 (2) - Transforming Operators (0) | 2025.03.24 |
[Combine] Combine Operator 완전 정복하기 (1) - Combining Operators (0) | 2024.12.26 |
[Combine] Cancellable, AnyCancellable 개념 뿌시기 (3) | 2024.12.05 |
[Combine] AnyPublisher와 Type Erasure 개념 뿌시기 (1) | 2024.12.02 |