Transforming Operators의 핵심 개념
- Transforming operators는 upstream publisher로부터 받은 값들을 변형해서 downstream으로 전달하는 연산자입니다.
- 모든 operator는 publisher이며, upstream 이벤트를 받아서 조작한 후 downstream으로 전달합니다.
- 에러 처리가 필요한 경우가 아니라면 upstream의 에러를 그대로 downstream으로 전달합니다.
Map
map은 업스트림 퍼블리셔의 모든 값의 타입을 변환시키는 Operator입니다.
특징
map 연산자는 업스트림 퍼블리셔로부터 전달받은 각 요소를 지정된 클로저를 사용해 변환합니다.
Swift 문법 map 함수와 비슷하게 동작하고, 한 종류의 데이터를 다른 종류로 변환할 때 사용됩니다.
구성
public func map<Result>(
_ transform: @escaping (Output) -> Result
) -> Publishers.Map<Self, Result> {
return Publishers.Map(upstream: self, transform: transform)
}
transform 클로저는 업스트림 Publisher의 Output을 인자로 가지고 있는데 이 클로저에서 원하는 형태의 타입으로 리턴시켜주면, Publishers.Map<Self, T> 타입으로 Publisher가 변환됩니다.
구조
extension Publishers {
/// A publisher that transforms all elements from the upstream publisher with
/// a provided closure.
public struct Map<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Upstream.Failure
/// The publisher from which this publisher receives elements.
public let upstream: Upstream
/// The closure that transforms elements from the upstream publisher.
public let transform: (Upstream.Output) -> Output
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) -> Output) {
self.upstream = upstream
self.transform = transform
}
public func receive<Downstream: Subscriber>(subscriber: Downstream)
where Output == Downstream.Input, Downstream.Failure == Upstream.Failure
{
upstream.subscribe(Inner(downstream: subscriber, map: transform))
}
}
결국 Publishers.Map<Self, T> 는 Publishers.Map<Upstream, Ouput> 이고,
receive 메서드에서 subscriber을 전달받을 때에는 Map의 Output이 Subscriber의 Input, 그리고 Upstream의 Failure이 Subscriber의 Failure이 전달됩니다
사용
사용 예시: 로마 숫자로 변환하기
let numbers = [5, 4, 3, 2, 1, 0]
let romanNumeralDict: [Int : String] = [1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
let cancellable = numbers.publisher // 스트림 시작
.map { romanNumeralDict[$0] ?? "(unknown)" } // 각 Int를 로마자로 변환
.sink { print("\\($0)", terminator: " ") } // 결과출력
// 출력: "V IV III II I (unknown)"
map 연산자는 숫자 배열의 각 정수를 로마 숫자로 변환하고 변환할 수 없는 경우에는 "(unknown)"을 반환하여 스트림을 계속 유지합니다.
- Int 배열을 publisher로 변환하여 스트림을 시작합니다.
- 여기서 numbers 배열이 publisher로 변환되어 데이터 스트림의 시작점이 됩니다.
- map 연산자를 사용하여 각 Int 값을 로마 숫자 문자열로 변환합니다.
- 스트림을 통해 전달되는 각 정수 값에 대해 romanNumeralDict 딕셔너리를 참조하여 해당하는 로마 숫자로 변환합니다. 만약 딕셔너리에 해당 키가 존재하지 않으면 "(unknown)" 문자열을 사용합니다.
- sink를 사용하여 구독을 생성하고, 스트림을 구독하여 결과를 출력합니다.
- 구독이 시작되면 스트림이 활성화되고, 최종적으로 "V IV III II I (unknown)"이 출력됩니다.
흐름
TryMap
업스트림 퍼블리셔로부터 모든 요소를 제공된 에러를 던질 수 있는 클로저로 변환하는 퍼블리셔
public struct TryMap<Upstream: Publisher, Output>: Publisher {
public typealias Failure = Error
/// 이 퍼블리셔가 요소를 수신하는 원본 퍼블리셔.
public let upstream: Upstream
/// 원본 퍼블리셔의 요소를 변환하는 에러를 던질 수 있는 클로저.
public let transform: (Upstream.Output) throws -> Output
public init(upstream: Upstream,
transform: @escaping (Upstream.Output) throws -> Output) {
self.upstream = upstream
self.transform = transform
}
}
map이랑 비슷한데 앞에 try가 붙었다는 차이가 있습니다.
네
public func tryMap<Result>(
_ transform: @escaping (Output) throws -> Result
) -> Publishers.TryMap<Upstream, Result> {
return .init(upstream: upstream) { try transform(self.transform($0)) }
}
}
이런식으로 에러를 던질수가 있습니다~!
예시
import Combine
//에러정의
enum SoptError: Error {
case invalidNumber(Int)
}
let numbers = [5, 4, 3, 2, 1, 0]
let romanNumeralDict: [Int : String] = [1:"I", 2:"II", 3:"III", 4:"IV", 5:"V"]
// 변환 함수 정의 : num이 nil이면 SoptError 던짐
func handleNumber(_ num: Int) throws -> String {
guard let roman = romanNumeralDict[num] else {
throw SoptError.invalidNumber(num)
}
return roman
}
let cancellable = numbers.publisher
.tryMap { num in
try handleNumber(num) // 에러가 발생할 수 있는 변환 수행
}
.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("\\n모든 변환이 완료되었습니다.")
case .failure(let error):
print("\\n에러 발생: \\(error)")
}
},
receiveValue: { value in
print(value, terminator: " ")
}
)
// 출력:
// V IV III II I
// 에러 발생: invalidNumber(0)
handleNumber가 에러를 던질 수 있게 만들어졌고, 만약 num이 nil이면 SoptError을 던지게 해놨어요.
없는 값 만남 > SoptError던짐 > tryMap이 에러 받음 > failure 발생> publish종료.
이런 flow 입니다.
흐름
여기까지 요약
map vs tryMap:
- map: 단순 변환 시 사용. 변환 실패 시 기본값을 반환하거나, 에러를 던지지 않음.
- tryMap: 변환 과정에서 에러를 던질 수 있음. 에러 발생 시 스트림이 .failure 상태로 종료되어 즉시 에러를 처리할 수 있음.
flatMap
trymap이 try하는 map이였으니까 flatmap은 flat한 맵이겟다(?)
정의
public func flatMap<Result, Child: Publisher>(
maxPublishers: Subscribers.Demand = .unlimited,
_ transform: @escaping (Output) -> Child
) -> Publishers.FlatMap<Child, Self>
where Result == Child.Output, Failure == Child.Failure {
return .init(upstream: self,
maxPublishers: maxPublishers,
transform: transform)
}
maxPublisher로 최대로 동시에 가질 수 있는 Publisher의 수를 지정하거나 아니면 무제한으로 지정할 수 있습니다(기본값은 무제한)
transform 클로저는 새로운 Publisher(Publishers.FlatMap)를 리턴한다.
특징
- 이벤트가 정상적으로 완료되어도 전체 스트림이 완료되지는 않는다
- 하지만 flatMap이 실패(Publisher변환이 실패)하면 전체 스트림이 실패처리된다.
- Demand 값을 조절할 수 있다.
Publishers.FlatMap 의 receive(subscriber:) 부분을 보면
public struct FlatMap<NewPublisher, Upstream> : Publisher where
NewPublisher : Publisher,
Upstream : Publisher,
NewPublisher.Failure == Upstream.Failure
{
public func receive<S>(subscriber: S) where S : Subscriber,
NewPublisher.Output == S.Input,
Upstream.Failure == S.Failure
}
NewPublisher의 Output을 Subscriber의 Input으로, Upstream의 Failure은 Subscriber의 Failure로 삼게됩니다!
→ Publisher(NewPublisher)의 Output 타입은 구독자(Subscriber)가 받을 수 있는 Input 타입과 같아야 한다
⇒ 데이터 타입과 에러 타입이 서로 호환되도록 보장된다! (에러핸들링이 편해진다)
사용
public struct WeatherStation {
public let stationID: String
}
//3개의 값이 publish된다고 가정
var weatherPublisher = PassthroughSubject<weatherstation, urlerror="">() // 1
let cancellable = weatherPublisher
.flatMap { station -> URLSession.DataTaskPublisher in // 2
let url = URL(string:"<https://weatherapi.example.com/stations/\\(station.stationID)/observations/latest>")!
return URLSession.shared.dataTaskPublisher(for: url)
}
.sink( // 3
receiveCompletion: { completion in
switch completion {
case .finished:
print("finished")
case .failure(let failure):
print(failure)
}
},
receiveValue: { value in
print(value)
}
)
//출력: 여러 도시의 WeatherStation을 발행
weatherPublisher.send(WeatherStation(stationID: "KSFO")) // San Francisco, CA
weatherPublisher.send(WeatherStation(stationID: "EGLC")) // London, UK
weatherPublisher.send(WeatherStation(stationID: "ZBBB")) // Beijing, CN
</weatherstation,>
- WeatherStation 타입을 받는 PassthroughSubject를 통해 퍼블리셔 발행
- 이를 flatMap을 통해 URLSession.DataTaskPublisher 로 변환한다.
- sink는 이제 초기 Publisher의 Output인 WeatherStation이 아니라**,** flatMap에서 리턴되는 새로운 Publisher인 ****URLSession.DataTaskPublisher를 구독합니다.
결론
즉, Publisher가 또 다른 Publisher를 반환할 때 발생하는 중첩을 평탄화(flat) 하게 만들고 (Publisher<Publisher<String, Error>, Error> → Publisher<String, Error> )
- 네트워크 요청처럼 각각이 Publisher를 반환하는 비동기 작업들을 연결할 때 사용
- WeatherStation 케이스처럼, 각 날씨 정보에 대해 새로운 네트워크 요청을 만들 때 사용
CompactMap
public func compactMap<ElementOfResult>(
_ transform: @escaping (Output) -> ElementOfResult?
) -> Publishers.CompactMap<Self, ElementOfResult> {
return .init(upstream: self, transform: transform)
}
기능:
- 입력 스트림의 각 요소를 클로저에 전달하고, 해당 클로저가 반환한 값이 nil이 아니면 그대로 방출.
- nil 값은 필터링되어 스트림에서 제외.
→ compactMap은 nil을 걸러내고 싶을 때 사용하는 연산자이다!
사용
let numbers = (0...5)
let romanNumeralDict: [Int: String] = [1: "I", 2: "II", 3: "III", 5: "V"]
let cancellable = numbers.publisher
.compactMap { romanNumeralDict[$0] } // 딕셔너리에서 값 조회, 없으면 nil 반환
.sink { print("\\($0)", terminator: " ") }// 0과 4는 딕셔너리에 없어서 nil -> 필터링됨
// 출력: "I II III V"
코드 설명:
- 숫자 배열을 Publisher로 변환.
- 각 숫자를 romanNumeralDict에서 키로 사용해 값을 검색. 값이 없으면 nil 반환.
- nil 값은 필터링되고, 나머지 값은 출력.
엥 map이랑 compactmap이랑은 그러면 뭐가 다른거지?
// map 예제
let numbers = PassthroughSubject<String, Never>()
numbers
.map { Int($0) } // 결과는 Optional<Int>
.sink { print($0) }
numbers.send("123") // Optional(123)
numbers.send("abc") // nil
---
// compactMap 예제
let numbers = PassthroughSubject<String, Never>()
numbers
.compactMap { Int($0) } // nil이 아닌 Int 값만 전달
.sink { print($0) }
numbers.send("123") // 123
numbers.send("abc") // 아무것도 출력되지 않음
- map:
nil 값을 그대로 downstream으로 전달, Optional을 포함한 모든 타입 가능
- compactMap:
nil 값을 필터링하여 제거, non-Optional 타입만 전달
scan
구성
public func scan<T>(
_ initialResult: T,
_ nextPartialResult: @escaping (T, Publishers.Sequence<Elements, Failure>.Output) -> T
) -> Publishers.Sequence<[T], Failure>
scan은 클로저가 반환한 마지막 값과 현재 값을 클로저에 제공하여 업스트림 Publisher의 요소를 반환하는 Operator
⇒ swift 기본 함수인 reduce 와 비슷한 개념!
기능
- 발행된 값에 대해 실행될 클로저(nextPartialResult) 가 존재.
- 이 클로저에서 이전값(없으면 scan의 initialResult)을 인자로 받고 새로운 값 발행.
- 각 단계에서 축적된 결과를 순차적으로 발행.
nextPartialResult의 결과값 T는 다음 연산의 initialResult로 사용되며, Publisher의 모든 값이 처리될 때까지 이 과정이 반복됩니다.
사용
let arrayPublisher = [1,2,3,4,5].publisher
arrayPublisher.scan(0) { result, value in
return result + value // 이전 결과값과 현재값을 더하기
}
.sink { value in
print(value)
}
.store(in: &cancellables)
// 결과
1
3
6
10
15
scan의 인자에는 숫자가 들어가는데, 이것이 초기값으로 사용됩니다.
result에는 클로저에서 리턴하는 결과값이 들어가게 됩니다.
그래서 0+1, 1+2, 3+3, 6+4, 10+5 의 결과를 보여주게 됩니다.
reduce와 매커니즘은 같지만, reduce는 연산의 완료값 (마지막 값인 15)만 나오게 되지만
- scan은 실시간으로 데이터를 처리하고 결과를 즉시 사용할 수 있게 해줍니다
- reduce는 최종 결과값만 반환하는 반면, scan은 모든 중간 계산 결과를 발행합니다
replaceNil
구성
public func replaceNil<ElementOfResult>(
with output: ElementOfResult
) -> Publishers.Sequence<[Elements.Element], Failure>
where Elements.Element == ElementOfResult?
{
return .init(sequence: sequence.map { $0 ?? output })
}
replaceNil은 Optional 값에서 nil을 대체값으로 교체하여 non-optional 값을 반환하는 Operator입니다.
⇒ Swift의 nil 병합 연산자(??)를 Publisher에 적용한 것과 같은 개념!
기능
- Optional 값을 가진 Publisher에서 nil이 발생할 경우 지정된 기본값으로 대체
- nil이 아닌 값은 그대로 다운스트림으로 전달
- Optional 타입을 non-optional 타입으로 변환
예시
let numbers = [1, nil, 3, nil, 5].publisher
numbers
.replaceNil(with: 0) // nil 값을 0으로 대체
.sink { print("\\\\($0)", terminator: " ") }
// 출력: "1 0 3 0 5"
- Optional Int 배열을 Publisher로 변환
- nil 값을 0으로 대체
- 결과적으로 모든 nil이 0으로 대체되어 출력
흐름
전체 요약
Operator 주요 기능 특징 사용 예시
Operator | 주요기능 | 특징 | 예시 |
Map | 데이터 타입변환 | • 단순 변환 수행 • 에러를 던지지 않음 • 실패 시 기본값 반환 가능 |
Int → String 변환 (숫자를 로마 숫자로) |
tryMap | 에러처리가 가능한 변환 | • map과 유사하나 에러 처리 가능 • 에러 발생 시 스트림 종료 • failure 상태로 전환 |
데이터 변환 중 예외 처리 필요 시 |
flatMap | 중첩된 Publisher 평탄화 | • Publisher를 반환하는 Publisher를 단일 스트림으로 • maxPublishers로 동시 처리 제한 가능 • 개별 이벤트 완료되어도 전체 스트림 유지 |
네트워크 요청 체이닝 |
compactMap | nil필터링과 변환 | • nil 값 자동 필터링 • Optional 언래핑 • 성공한 변환만 downstream으로 전달 |
String → Int 변환 시 실패 케이스 제거 |
scan | 누적 계산 수행 | • 이전 결과값 활용한 계산 • 중간 결과 모두 방출 • reduce와 유사, 중간값 제공 |
실시간 합계 계산 |
replacenil | nil값 대체 | • Optional 값의 nil을 지정값으로 대체 • non-optional로 변환 • nil 아닌 값은 그대로 전달 |
nil 값을 기본값으로 대체 |
'IOS - Swift > Combine' 카테고리의 다른 글
[iOS/Combine] Combine - Error Handling (0) | 2024.12.12 |
---|---|
subscription의 이해 (0) | 2024.11.25 |
Publisher - Just, Empty, Failure (1) | 2024.10.03 |
Combine - Publisher 기초 (2) | 2024.10.01 |
[iOS/Swift] Combine - 시작하기 (Publisher ,Operator, Subscriber) (1) | 2024.04.17 |