Operator Guide
저번 글에서는 Combine의 Operator가 무엇인지, 어떤 역할을 하는지 간단히 살펴봤습니다.
이번 글에서는 그 연장선으로, 자주 사용되는 오퍼레이터들을 종류별로 정리해보려 합니다.
Operator는 흔히 변환 연산자, 필터 연산자, 조합 연산자로 분류되지만,
이 구분은 완전히 명확하게 나뉘는 구조는 아니며, 각 오퍼레이터가 여러 역할을 수행하는 경우도 많습니다.
예를 들어 compactMap은 값을 변환하면서도 nil 값을 거르는 필터링 역할도 함께 합니다.
그럼, 각 분류별 주요 오퍼레이터들을 살펴보겠습니다.
변환연산자
Combine의 변환 연산자는 Publisher에서 방출된 값을 다른 값으로 바꾸는 역할을 합니다.
입력된 스트림 데이터를 원하는 형식으로 가공하거나, 비동기 작업을 연결하는 데에 자주 사용됩니다.
Map
map은 Combine에서 가장 자주 쓰이는 변환 연산자 중 하나입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Transforms all elements from the upstream publisher with a provided closure.
(퍼블리셔가 보내주는 데이터 하나하나를 내가 정한 방식(클로저)으로 바꿔주는 역할을 한다.)
즉, map은 Publisher가 방출하는 각 값을 클로저로 변환해서 새로운 값으로 바꾸고, 그 값을 다음 연산자로 넘깁니다.
import Combine
let numbers = [3, 6, 9].publisher
let subcriber = numbers
.map { $0 * 2 }
.sink { print("출력 값: \($0)")}
// 출력:
// 출력 값: 6
// 출력 값: 12
// 출력 값: 18
TryMap
tryMap은 map과 매우 유사하지만, 변환 과정에서 에러가 발생할 수 있는 경우에 사용합니다.
애플 공식 문서에서는 이렇게 설명합니다:
Transforms all elements from the upstream publisher with a provided error-throwing closure.
(퍼블리셔가 보내는 값을, 에러를 던질 수 있는 클로저를 이용해 변환합니다.)
일반 map은 에러 처리를 할 수 없지만, tryMap은 변환 도중 문제가 생길 수 있을 때 이를 명시적으로 처리할 수 있습니다.
변환 중 발생한 에러는 .failure를 통해 downstream에 전달되고, 스트림은 중단됩니다.
import Combine
enum MathError: Error {
case divideByZero
}
let numbers = [4, 2, 0, 8].publisher
numbers
.tryMap { value -> Int in
guard value != 0 else {
throw MathError.divideByZero
}
return 10 / value
}
.sink(receiveCompletion: { completion in
print("completion:", completion)
}, receiveValue: { result in
print("결과:", result)
})
// 출력:
// 결과: 2
// 결과: 5
// completion: failure(divideByZero)
FlatMap
flatMap은 Combine에서 Publisher 안의 값을 또 다른 Publisher로 바꾸고,
그 결과들을 하나의 스트림으로 평탄화(flatten) 해서 전달해주는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Transforms all elements from an upstream publisher into a new publisher up to a maximum number of publishers you specify.
(upstream publisher의 값을 새로운 publisher로 변환한 후, 이들을 하나의 publisher로 결합해서 방출합니다.)
map과 비슷하지만, 반환 값이 Publisher일 때 사용합니다. 내부에서 생성된 여러 Publisher를 하나의 흐름으로 연결할 수 있습니다.
주로 비동기 작업 체이닝에 많이 쓰입니다.
예를 들어, “A 결과로 B 요청을 또 보내야 하는 상황”→ 이런 흐름을 flatMap으로 간단히 표현할 수 있습니다.
import Combine
let words = ["Hi", "Bye"].publisher
words
.flatMap { word in
word.map { String($0) }.publisher
}
.sink { letter in
print("글자:", letter)
}
// 출력:
// 글자: H
// 글자: i
// 글자: B
// 글자: y
// 글자: e
Reduce
reduce는 Combine에서 스트림의 모든 값을 하나로 누적해서, 마지막에 단 한 번 최종 결과만을 방출하는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Applies a closure that collects each element of a stream and publishes a final result upon completion.
(스트림의 요소들을 클로저로 누적하여 모은 뒤, 완료 시점에 한 번만 최종 결과를 방출합니다.)
스트림에서 전달되는 여러 값들을 하나의 값으로 합치고 싶을 때 사용합니다. Publisher가 완료되기 전까지는 아무 결과도 방출하지 않으며,완료 시점에 누적된 결과를 한 번만 출력합니다.
import Combine
let numbers = [1, 2, 3, 4].publisher
.reduce(0, +)
.sink { result in
print("최종 결과:", result)
}
// 출력:
// 최종 결과: 10
Collect
collect는 Publisher가 방출하는 여러 개의 값을 배열(Array) 로 묶어서 한 번에 방출해주는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Collects up to the specified number of elements, and then emits a single array of the collection.
(정해진 개수만큼 값을 모아서 배열 형태로 방출합니다.)
1. .collect()→ 스트림이 끝날 때까지 모든 값을 모아서 한 번에 배열로 방출
2. .collect(n)→ n개 단위로 값을 묶어서 여러 번 방출
import Combine
let numbers = [1, 2, 3, 4, 5].publisher
.collect(2)
.sink { print("묶음:", $0) }
// 출력:
// 묶음: [1, 2]
// 묶음: [3, 4]
// 묶음: [5]
필터연선자
앞서 소개한 변환 연산자는 값을 바꾸는 역할을 했습니다.
이번에는 Publisher가 방출하는 값들 중에서 필요한 것만 골라내는 필터 연산자에 대해 알아보겠습니다.
Filter
filter는 Combine에서 조건에 맞는 값만 통과시키는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Republishes all elements that match a provided closure.
(제공된 클로저 조건에 일치하는 값만 다시 방출합니다.)
filter는 Publisher가 방출하는 각 값을 검사해서, 클로저의 결과가 true인 값만 통과시키고 나머지는 제거합니다.
불필요한 데이터를 걸러내거나, 특정 조건을 만족하는 값만 사용하고 싶을 때 유용합니다.
import Combine
let numbers = [1, 2, 3, 4, 5].publisher
numbers
.filter { $0 % 2 == 0 }
.sink { print("받은 값: \($0)") }
// 출력:
// 받은 값: 2
// 받은 값: 4
CompactMap
compactMap은 Combine에서 옵셔널 값을 안전하게 처리하면서 nil은 제거하는 필터 + 변환 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Calls a closure with each received element and publishes any returned optional that has a value.
(클로저에서 반환된 옵셔널 값 중 nil이 아닌 값만 방출합니다.)
map처럼 값을 변환하면서 filter처럼 nil 값은 제거합니다. 즉, 옵셔널 값이 포함된 스트림을 안전하게 정리할 때 매우 유용합니다.
import Combine
let names: [String?] = ["Alice", nil, "Bob", nil, "Charlie"]
let publisher = names.publisher
publisher
.compactMap { $0 }
.sink { print("이름:", $0) }
// 출력:
// 이름: Alice
// 이름: Bob
// 이름: Charlie
RemoveDuplicates
removeDuplicates()는 Combine에서 연속으로 중복되는 값을 걸러내는 필터 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Publishes only elements that don’t match the previous element.
(이전 값과 다른 값만 방출하고, 같은 값은 제거합니다.)
이 연산자는 연속된 값 중 중복되는 값은 무시하고 새로운 값이 나올 때만 그 값을 전달합니다.
단순히 전체 중복 제거가 아니라 이전 값과 같은 경우만 제거한다는 점에 유의해야 합니다.
import Combine
let numbers = [1, 1, 2, 2, 3, 1, 1].publisher
.removeDuplicates()
.sink { print("받은 값:", $0) }
// 출력:
// 받은 값: 1
// 받은 값: 2
// 받은 값: 3
// 받은 값: 1
Perfix
prefix는 Combine에서 지정한 개수만큼의 값만 통과시키고 나머지는 무시하는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Republishes elements up to the specified maximum count.
(지정한 개수까지만 방출하고, 이후 값은 무시합니다.)
Publisher가 방출하는 값 중 앞에서부터 N개까지만 전달하고 이후 값은 무시하고 스트림을 종료합니다.
주로 처음 일부 값만 확인하거나, 미리보기, 샘플링, 테스트 등에 활용됩니다.
import Combine
let numbers = [10, 20, 30, 40].publisher
.prefix(2)
.sink { print("받은 값:", $0) }
// 출력:
// 받은 값: 10
// 받은 값: 20
IgnoreOutput
ignoreOutput()은 Combine에서 모든 값을 무시하고 완료 상태만 전달하는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Ignores all upstream elements, but passes along the upstream publisher’s completion state (finished or failed).
(upstream의 값은 무시하고, 완료 또는 실패 상태만 downstream에 전달합니다.)
이 연산자는 값 자체는 전혀 전달하지 않고, 오직 .finished 또는 .failure 이벤트만 전달합니다.
주로 데이터 자체는 중요하지 않고, 흐름의 완료 여부만 확인할 때 사용됩니다. 테스트나 흐름 제어 목적으로 자주 활용됩니다.
import Combine
let numbers = [100, 200, 300].publisher
.ignoreOutput()
.sink(
receiveCompletion: { print("완료: \($0)") },
receiveValue: { _ in print("이건 출력되지 않음") }
)
// 출력:
// 완료: finished
조합연산자
지금까지는 하나의 Publisher에서 방출되는 값을 변환하거나 필터링하는 연산자들을 살펴봤습니다.
이번에는 여러 Publisher를 결합하거나, 스트림 흐름을 제어하는 연산자인 조합 연산자에 대해 알아보겠습니다.
Merge
merge는 여러 Publisher의 이벤트를 하나의 스트림으로 결합해주는 조합 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Combines elements from this publisher with those from two other publishers, delivering an interleaved sequence of elements.
(이 Publisher와 다른 Publisher들의 값을 섞어서 하나의 스트림으로 방출합니다.)
merge는 여러 개의 Publisher가 동시에 값을 보낼 수 있을 때, 이들을 순서에 상관없이 하나의 스트림으로 통합해서 방출합니다.
각각의 Publisher에서 독립적으로 이벤트가 발생하더라도 하나의 흐름으로 이어 붙여 처리할 수 있습니다.
예를 들어, A와 B 두 Publisher에서 각각 값이 들어오면, 이를 하나의 흐름으로 묶어 받아볼 수 있습니다.
import Combine
let numbers1 = [1, 2, 3].publisher
let numbers2 = [4, 5, 6].publisher
Publishers.Merge(numbers1, numbers2)
.sink { print("받은 값: \($0)") }
// 출력:
// 받은 값: 1
// 받은 값: 2
// 받은 값: 3
// 받은 값: 4
// 받은 값: 5
// 받은 값: 6
CombineLatest
combineLatest는 두 개 이상의 Publisher가 최신 값을 가졌을 때, 이들의 값을 쌍으로 묶어서 방출하는 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Subscribes to an additional publisher and publishes a tuple upon receiving output from either publisher.
(두 Publisher 중 하나라도 값을 emit하면, 각 Publisher의 최신값을 결합해서 튜플로 방출합니다.)
combineLatest는 모든 Publisher가 최소 한 번 값을 방출한 이후부터 작동합니다.
그 후에는 각 Publisher가 새로운 값을 방출할 때마다, 다른 Publisher의 최신값과 함께 튜플 형태로 출력됩니다.
UI 상태 갱신, 양방향 입력 처리, 네트워크 요청 결과 병합 등 실무에서 매우 자주 사용됩니다.
예를 들어, 텍스트 필드 A와 B의 값이 각각 변경될 때마다, 둘의 최신값을 묶어서 서버에 전송하거나 버튼 상태를 판단할 수 있습니다.
import Combine
let numbers1 = [1, 2, 3].publisher
let numbers2 = [4, 5, 6].publisher
numbers1.combineLatest(numbers2)
.sink { print("받은 값: \($0)") }
// 출력:
// 받은 값: (3, 4)
// 받은 값: (3, 5)
// 받은 값: (3, 6)
먼저 numbers1이 [1, 2, 3]을 순차적으로 모두 방출하며→ 가장 마지막 값 3이 최신값으로 유지합니다.
그 후 numbers2가 [4, 5, 6]을 방출합니다. 이때마다 combineLatest는 numbers1의 최신값 3과 numbers2의
새 값을 조합해 출력합니다.
Zip
zip은 두 Publisher의 값을 순서대로 짝지어 하나의 튜플로 방출하는 조합 연산자입니다.
애플 공식 문서에서는 이렇게 설명합니다:
Combines elements from two other publishers and delivers groups of elements as tuples.
(두 Publisher의 요소들을 묶어서 쌍으로 그룹화해 튜플로 전달합니다.)
zip은 각 Publisher가 값을 방출할 때마다 한 쌍씩 묶어서 출력합니다. 두 Publisher 중 한 쪽이라도 값이 부족하면
기다렸다가 짝이 맞는 순간 방출됩니다.
combineLatest는 최신값을 기준으로 동작하지만, zip은 순서와 쌍을 맞추는 것에 집중합니다.
import Combine
let numbers1 = [1, 2, 3].publisher
let numbers2 = [4, 5, 6].publisher
numbers1.zip(numbers2)
.sink { print("받은 값: \($0)") }
// 출력:
// 받은 값: (1, 4)
// 받은 값: (2, 5)
// 받은 값: (3, 6)
🧩 마무리
지금까지 Combine의 대표적인 Operator들을 세 가지 카테고리로 나누어 살펴봤습니다:
- 🔄 변환 연산자: map, tryMap, flatMap, reduce, collect
- 🔍 필터 연산자: filter, compactMap, removeDuplicates, prefix, ignoreOutput
- 🔗 조합 연산자: merge, combineLatest, zip
각 연산자는 Publisher가 방출하는 값의 흐름을 변형하거나 조절하기 위한 도구입니다.
Combine을 제대로 활용하려면 이 연산자들의 동작 원리와 사용 시점을 이해하는 것이 매우 중요합니다.
다음 글은 아직 확정되지 않았지만, UIKit vs SwiftUI, 또는 Combine을 실제 앱에 어떻게 적용할지에 대해 다뤄볼까
고민 중입니다.
궁금한 점이나 피드백은 언제든 댓글로 남겨주세요! 부족한 내용이지만 끝까지 읽어봐주셔서 감사합니다.
오늘도 개발자를 위한 명언으로 마무리하도록 하겠습니다! 파이팅입니다~
끊임없이 변하는 세상에서 가장 중요한 기술은 안목이다.
'iOS 개발 > Combine' 카테고리의 다른 글
[Combine] Operator, Scheduler (2) | 2025.05.02 |
---|---|
[Combine] 데이터를 처리하는 Subscriber (0) | 2025.04.28 |
[Combine] 데이터를 생산하는 Publisher (0) | 2025.04.25 |
[Combine] 왜 Combine을 알아야 할까? (0) | 2025.04.23 |