티스토리 뷰

반응형

안녕하세요 Pingu입니다.🐧

 

지난 글... 이 좀 오래됐는데, 어쨌든 지난 글에서는 Combine의 Publisher, Subscriber를 연결하는 Subscription에 대해 알아봤었는데, 이번 글에서는 드디어 Operator에 대해 알아보려고 합니다.

 

Combine에 Opeartor들이 얼마나 많은지...; 근데 그럼에도 불구하고 부족한 게 많아서 직접 만들어서 써야 하는 것들이 많은 거 같더라고요.

일단 기본부터 하자는 생각에 Apple에서 미리 만들어둔 Operator들을 먼저 공부해보도록 하겠습니다.

 

일단 Apple의 Publishers 공식문서에 보면 미리 구현해둔 Operator들이 모두 정리되어있는데요, 그중에 Mapping Elements라는 녀석들부터 알아보도록 하겠습니다.

 

Operator란?

일단 Operator들은 Publisher의 extension에 구현되어있습니다.

실제로 정의를 봐도 Publisher의 extension에 여러 개의 Operator들이 메서드로 구현되어있다고 되어있네요.

그리고 여기에 구현된 Operator역할을 하는 메서드들이 반환하는 타입이 Publishers에 구현되어있습니다. ("s"가 붙은 차이점을 잘 봐야 합니다.)

 

Operator를 사용하면 Upstream에게서 값을 받아서 원하는 대로 처리한 뒤 Downstream에 값을 내려보낼 수 있습니다.

그리고 지금부터 알아볼 미리 구현된 Operator들은 어떤 일을 하는지 미리 정해져 있습니다. (물론 직접 Operator를 만들 수도 있습니다.)

그래서 다양한 역할을 하는 여러 개의 Operator들을 활용해서 원하는 값을 만들어낼 수 있게 됩니다.

 

그리고 Operator인 이유는 이후에 알아볼 Map이라는 Operator의 구현에서도 알 수 있습니다...

public struct Map<Upstream, Output> : Publisher where Upstream : Publisher {
	...
}

구현에 나와있듯이 Operator의 반환 타입으로 사용할 수 있는 Publisher는 Upstream이 반드시 Publisher여야 합니다.

 

그래서 이번 글에서는 Apple이 구현해둔 많은 Operator들 중 Mapping Elements라고 분류된 녀석들에 대해 알아보려고 합니다.

 

Mapping Elements

Mapping Elements Operator가 반환하는 Publisher에는 아래와 같은 타입들이 있습니다.

  • Map
  • TryMap
  • MapError
  • Scan
  • TryScan
  • SetFailureType

그리고 위의 6개의 Publisher를 반환하는 Operator는 아래와 같이 7개입니다.

  • map(_:)
  • tryMap(_:)
  • mapError(_:)
  • replaceNil(with:)
  • scan(_:_:)
  • tryScan(_:_:)
  • setFailureType(to:)

이번 글에서는 방금 언급한 Publisher들과 Operator들을 알아보도록 하겠습니다!

Map

정의를 보면 제공된 클로저를 사용해서 Upstream Publisher의 모든 element를 변환하는 Publisher라고 되어있습니다.

 

그럼 바로 Map이라는 Publisher를 활용해서 만든 Operator를 알아볼게요.

 

map(_:)

map(_:)의 정의를 보면 Publishers.Map 타입을 반환하는 것을 볼 수 있습니다. 그리고 역할은 Upstream에서 받은 모든 값을 제공된 클로저로 변환해서 Downstream으로 보내는 역할을 한다고 합니다.

 

Swift의 기본 제공 함수 중에도 map이 있었는데, 얘도 비슷하게 작동하는 거 같아요.

 

간단하게 사용을 해보면 바로 이해가 됩니다.

let intPublisher = [1, 2, 3, 4, 5, 6, 7].publisher
intPublisher
    .map { element in
        return element * 2
    }
    .sink(receiveValue: { print($0) })

이렇게 하면 결과는 다음과 같습니다.

// Map 코드 결과
2
4
6
8
10
12
14

즉 Map은 upstream publisher이 값을 내보내면 그걸 받아서 자신의 클로저에 구현된 대로 수행한 뒤에 downstream으로 보내는 역할을 합니다. 위 코드에서는 받은 값을 2배로 증가시켜서 downstream으로 보냈으니 결과가 위와 같은 것입니다!

 

Map에는 key path를 사용해서 1~3개의 프로퍼티에 매핑할 수 있게 만들 수도 있는데요, 간단하게 사용해보면 다음과 같습니다.

struct Point {
    let x: Int
    let y: Int
    let z: Int
}

let publisher = PassthroughSubject<Point, Never>()

publisher
    .map(\.x, \.y, \.z)
    .sink(receiveValue: { x, y, z in
        print("x: \(x), y: \(y), z: \(z)")
    })

publisher.send(Point(x: 1, y: 2, z: 3))

"4개는 외 않 돼?"라고 생각하신다면,, 미리 정의된 거에는 다음과 같이 3개까지 밖에 없어서 그렇습니다.

public func map<T>(_ keyPath: KeyPath<Self.Output, T>) -> Publishers.MapKeyPath<Self, T>
public func map<T0, T1>(_ keyPath0: KeyPath<Self.Output, T0>, _ keyPath1: KeyPath<Self.Output, T1>) -> Publishers.MapKeyPath2<Self, T0, T1>
public func map<T0, T1, T2>(_ keyPath0: KeyPath<Self.Output, T0>, _ keyPath1: KeyPath<Self.Output, T1>, _ keyPath2: KeyPath<Self.Output, T2>) -> Publishers.MapKeyPath3<Self, T0, T1, T2>

4개부터는 직접 만들어야 할 것 같아요.

 

뭐 어쨌든 Map은 간단하게 여기까지만 알아보면 될 거 같네요!

TryMap

이번엔 TryMap입니다.

TryMap은 이름부터 map이랑 똑같을 거면서 try를 사용해서 에러를 downstream으로 보낼 수 있을 것만 같이 생겼습니다.

실제로 정의를 봐도 그렇습니다.

tryMap(_:)

TryMap을 활용해서 만든 Operator는 tryMap(_:)입니다.

역할은 아까 알아본 map(_:)에서 에러도 던질 수 있다는 차이밖에 없습니다.

 

간단하게 사용해보면 아래와 같아요.

enum PinguError: Error {
    case elementIsNil
}
func checkNil(element: Int?) throws -> Int {
    guard let element = element else {
        throw PinguError.elementIsNil
    }
    return element
}

let publisher = [1, 2, nil, 4].publisher
publisher
    .tryMap { try checkNil(element: $0) }
    .sink(receiveCompletion: {
        switch $0 {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("끝~")
        }
    }, receiveValue: { print($0) })

이렇게 에러를 downstream으로 보낼 수 있습니다.

실제로 위 코드는 에러를 보내게 되는데요, 결과는 다음과 같이 나옵니다.

1
2
The operation couldn’t be completed. (__lldb_expr_39.(unknown context at $107ab7534).(unknown context at $107ab7570).(unknown context at $107ab7578).PinguError error 0.)

결과를 보면 publisher가 가지고 있던 값은 4개인데, 3번째 element에서 에러를 받으면 failure를 받아 publish가 끝나서 4번째 값은 방출이 안된 것을 볼 수 있습니다.

MapError

아까 TryMap에서는 받아온 에러의 타입을 바꾸는 건 하지 못했는데요, MapError를 사용하면 upstream에서 발생한 에러를 받아서 원하는 에러 타입에 매핑할 수 있습니다.

mapError(_:)

MapError Publisher를 활용해서 만든 mapError(_:)는 Upstream에서 받은 failure를 새로운 에러로 바꾸는 것이라고 되어있네요.

 

간단하게 사용해보면 다음과 같이 사용할 수 있습니다.

enum AnyError: Error {
    case any
}

enum PinguError: Error {
    case elementIsNil
}

func checkNil(element: Int?) throws -> Int {
    guard let element = element else {
        throw AnyError.any
    }
    return element
}

let publisher = [1, 2, nil, 4].publisher
publisher
    .tryMap { try checkNil(element: $0) }
    .mapError { $0 as? PinguError ?? .elementIsNil }
    .sink(receiveCompletion: {
        switch $0 {
        case .failure(let error):
            if error == .elementIsNil {
                print("elementIsNil 에러")
            } else {
                print(error.localizedDescription)
            }
        case .finished:
            print("끝~")
        }
    }, receiveValue: { print($0) })

아까 코드와 거의 비슷한데, 이번에는 AnyError라는 타입도 추가되었고, checkNil(element:) 함수에서 값이 nil일 때 AnyError가 반환됩니다. 

 

그런 뒤 아까와 마찬가지로 tryMap을 사용해서 downstream으로 에러를 내려보냅니다.

그런데 그렇게 내려보낸 downstream이 mapError였고, 거기서 AnyError를 PinguError의 elementIsNil로 바꿉니다.

 

실행을 시켜보면 다음과 같이 잘 변경된 것을 볼 수 있습니다.

1
2
elementIsNil 에러

Scan

다음은 Scan입니다.

정의를 보면 upstream publisher의 element를 closure에서 반환한 마지막 값과 함께 내보내는 것이라고 되어있습니다.

 

Scan의 구현을 보면 아래와 같은데요,

public struct Scan<Upstream, Output> : Publisher where Upstream : Publisher {
    public typealias Failure = Upstream.Failure

    public let upstream: Upstream

    public let initialResult: Output

    public let nextPartialResult: (Output, Upstream.Output) -> Output

    public init(upstream: Upstream, initialResult: Output, nextPartialResult: @escaping (Output, Upstream.Output) -> Output)
	public func receive<S>(subscriber: S) where Output == S.Input, S : Subscriber, Upstream.Failure == S.Failure
}

 

생성자를 보면 initialResult값(Output과 같은 타입)과 nextPartialResult값(클로저)이 필요하다는 것을 알 수 있습니다.

scan(_:_:)

Scan Publisher을 활용해서 만든 scan(_:_:)은 Upstream에서 받은 값을 클로저에서 반환한 마지막 값과 함께 내보낸다고 되어있습니다. 뭔가 글로 보면 이해가 어려운데, 예제와 실행 결과를 보니 이해가 아주 쉽게 됐습니다.

 

간단하게 사용해보면 다음과 같습니다.

let publisher = [1, 2, 3, 4, 5].publisher

publisher
    .scan(0) { (latest, current) -> Int in
        print("latest: \(latest), current: \(current)")
        return latest + current
    }
    .sink(receiveValue: { print($0) })

실행해보면 결과는 다음과 같아요.

latest: 0, current: 1
latest: 1, current: 2
latest: 3, current: 3
latest: 6, current: 4
latest: 10, current: 5
1
3
6
10
15

가장 최근에 내보낸 값과 현재 값을 클로저에서 사용해서 downstream으로 내려보내는 것을 볼 수 있습니다.

위 코드는 가장 최근에 내보낸 값과 현재 값을 더해서 downstream으로 내려보내고 있는데, 그러다 보니 마지막 결과가 15로 publisher의 모든 값을 더한 값과 같아진 것을 볼 수 있네요.

TryScan

TryMap과 비슷하게 TryScan도 Scan이랑 똑같으면서 에러를 내려보낼 수 있는 Publisher 같이 보이네요.

실제로 정의를 봐도 그렇습니다. 현재 값과 최근 값을 가지고 노는 클로저가 에러를 반환할 수 있는 것만 다릅니다.

 

실제 구현을 봐도 nextPartialResult에 throws 키워드만 붙어있는 차이만 있습니다.

public struct TryScan<Upstream, Output> : Publisher where Upstream : Publisher {
    public typealias Failure = Error

    public let upstream: Upstream

    public let initialResult: Output

    public let nextPartialResult: (Output, Upstream.Output) throws -> Output
	
    public init(upstream: Upstream, initialResult: Output, nextPartialResult: @escaping (Output, Upstream.Output) throws -> Output)

    public func receive<S>(subscriber: S) where Output == S.Input, S : Subscriber, S.Failure == Publishers.TryScan<Upstream, Output>.Failure
}

tryScan(_:_:)

TryScan Publisher를 활용해서 만든 Operator는 tryScan(_:_:)입니다.

정의를 보면 아까 사용해본 scan(_:_:)에서 에러만 던질 수 있는 차이점밖에 없네요.

 

사용해보면 다음과 같이 사용할 수 있습니다.

enum PinguError: Error {
    case evenNumber
}

func checkEvenNumberAndSum(_ latest: Int, _ current: Int) throws -> Int {
    guard current % 2 != 0 else { throw PinguError.evenNumber }
    return latest + current
}

let publisher = [1, 2, 3, 4, 5].publisher

publisher
    .tryScan(0) { latest, current in
        try checkEvenNumberAndSum(latest, current)
    }
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) }
    )

결과는 다음과 같습니다.

1
failure(__lldb_expr_62.(unknown context at $103ea62bc).(unknown context at $103ea638c).(unknown context at $103ea6394).PinguError.evenNumber)

코드를 보면 홀수일 때만 가장 최근 값에 현재 값을 더하고, 짝수일 때는 에러를 내려보냅니다.

그러다 보니 publisher의 element 중 첫 번째 값인 1만 더해진 뒤에 에러가 발생해서 publisher가 failure로 끝난 것을 볼 수 있습니다.

SetFailureType

오늘 알아볼 마지막 Publisher는 SetFailureType입니다.

정의를 보면 실패하지 않는 타입의 publisher를 특정 에러 타입을 보낼 수 있는 타입의  publisher로 바꾸는 역할을 한다고 합니다.

setFailureType(to:)

SetFailureType Publisher를 활용해서 만든 Operator는 setFailureType(to:)입니다.

정의를 보면 Upstream의 failure 타입을 바꾸는 역할을 한다고 되어있습니다.

그런데 Upstream의 Failure 타입이 Never일 때만 사용할 수 있다고 되어있습니다.

 

Failure타입이 Never라는 것은 실패하지 않는 타입이라는 건데요,, 이전 글에서 Just를 사용하면 Failure 타입이 Never인 publisher를 만들 수 있다고 했었습니다.

 

Just를 사용해서 간단하게 사용해보면 다음과 같습니다.

위 코드와 같이 PinguError라는 에러 타입을 하나 만들고, 에러 타입이 Never인 Just로 Publisher를 만든 뒤 setFailureType을 사용해 에러 타입이 PinguError인 Publisher로 만들어줍니다.

 

실제로 위와 같이 eraseToAnyPublisher()를 사용했을 때 반환되는 Publisher의 에러 타입이 PinguError인 것을 알 수 있습니다.

 

그럼 setFailureType으로 에러를 발생할 수 있는 Publisher로 만들어줘야 하는 이유가 뭘까요?

enum PinguError: Error {
    case pingu
}

let publisher = [1, 2, 3, 4].publisher
let pinguErrorPublisher = PassthroughSubject<Int, PinguError>()

publisher
    .setFailureType(to: PinguError.self) // <Int, PinguError> 타입으로 변경
    .combineLatest(pinguErrorPublisher) // Output, Failure 타입이 같기 때문에 combineLatest 사용 가능!
    .sink(receiveCompletion: { print($0) },
          receiveValue: { print($0) }
    )

pinguErrorPublisher.send(0)

// 실행 결과
(4, 0)

위 코드와 같이 combineLatest와 같이 여러 개의 publisher들을 함께 사용해야 하는 경우가 있습니다. 이런 경우에는 Publisher의 Output, Failure 타입이 같아야 사용이 가능한데요, 이럴 때 Failure 타입을 맞춰주기 위해 setFailureType을 사용하게 됩니다.

 

이렇게 Mapping Elements로 분류된 Operator들을 살펴봤습니다.

 

다음 글에서는 Filtering Elements로 분류된 Operator를 알아보지 않을까 싶네요.

사실 Operator가 너무 많아서 좀 당황스러웠는데,, 다 정리를 해보도록 하겠습니다.😞

 

아! 전체 코드는 여기에서 볼 수 있습니다.

 

감사합니다~!

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함