티스토리 뷰

반응형

안녕하세요 Pingu입니다.🐧

 

지난 글에서는 Combine의 Operator 중 Mapping Element 역할을 하는 녀석들을 알아봤었습니다. Map, TryMap, MapError, Scan, TryScan, SetFailureType이 Mapping Element로 분류된 Operator 들이었죠.

 

이번 글에서는 이어서 Filtering Element로 분류된 Operator에 대해 알아보도록 하겠습니다.

 

Filtering Elements

이름을 보면 뭔가를 필터링해줄 거 같은데요, 여기에 분류된 Publisher는 아래와 같습니다.

  • Filter
  • TryFilter
  • CompactMap
  • TryCompactMap
  • RemoveDuplicates
  • TryRemoveDuplicates
  • ReplaceEmpty
  • ReplaceError

위와 같이 8개의 Publisher가 Filtering Element로 분류되어있습니다.

 

그리고 이를 활용해서 만들어진 Operator는 아래와 같이 9개입니다.

  • filter(_:)
  • tryFilter(_:)
  • compactMap(_:)
  • tryCompactMap(_:)
  • removeDuplicates()
  • removeDuplicates(by:)
  • tryRemoveDuplicates(by:)
  • replaceEmpty(with:)
  • replaceError(with:)

뭐 이렇게 보면 여러 개 있는 거 같은데 지난 글에서 알아본 Operator와 같이 몇 개는 Try만 붙어서 에러를 처리만 할 수 있을 뿐 동일한 역할을 할 거 같긴 하네요.

 

그리고 Filtering이라는 이름에서 느낄 수 있는 것은 Upstream에서 받은 값들을 필터링해서 Downstream으로 전달하는 역할을 할 것 같습니다.

 

그럼 하나씩 차례대로 알아보도록 할게요.

 

Filter

정의를 보면 제공된 클로저를 사용해서 모든 element를 조건에 맞는 것만 내보내는 publisher라고 되어있습니다.

간단하게 그냥 전달받은 애들 중에 조건에 맞는 애들만 다시 내보내는 녀석입니다.

filter(_:)

Filter Publisher를 활용해서 만들어진 Operator는 filter(_:)입니다.

정의를 보면 Upstream에서 받은 값 중 클로저를 통과한 모든 값을 Downstream으로 내려보낸다고 되어있습니다.

 

사용해보면 바로 이해가 됩니다.

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

위 코드를 보면 짝수만 downstream으로 보내도록 만든 것을 볼 수 있는데요, 실제로 실행하면 결과는 아래와 같습니다!

Filter 예제 코드
2
4
6

TryFilter

그럼 Filter에 Try가 붙은 TryFilter도 알아보겠습니다.

예상대로 그냥 Filter인데 에러를 던질 수 있다는 차이점만 가지고 있습니다.

tryFilter(_:)

TryFilter Publisher로 만들어진 Operator는 tryFilter(_:)입니다.

정의를 보면 역시나 filter(_:)와 동일한데 에러를 던질 수 있다는 차이점밖에 없네요.

 

바로 사용해볼게요.

enum PinguError: Error {
    case oddNumber
}

func checkEvenNumber(_ number: Int) throws -> Bool {
    guard number % 2 == 0 else {
        throw PinguError.oddNumber
    }
    return true
}

let intPublisher = [2, 2, 4, 4, 5, 6].publisher
intPublisher
    .tryFilter { element in
        try checkEvenNumber(element)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("모두 짝수네용")
        }
    }, receiveValue: { print($0) })

위 코드처럼 코드를 작성하면 짝수일 때는 downstream으로 값을 내려보내고, 짝수가 아닐 때는 에러를 던지게 됩니다.

실행해보면 결과는 아래와 같습니다.

TryFilter 예제 코드
2
2
4
4
The operation couldn’t be completed. (__lldb_expr_9.(unknown context at $1093875b4).(unknown context at $1093875bc).(unknown context at $1093875c4).PinguError error 0.)

결과를 보면 2나 4는 짝수라서 잘 내려보내다가, 5에서 에러가 던져져서 실패로 끝난 것을 볼 수 있어요.

CompactMap

다음으로 CompactMap을 살펴볼게요.

정의를 보면.. upstream에서 받은 element를 클로저에 전달해서 처리된 값이 nil이 아닌 값들만 downstream으로 내려보내는 publisher인 듯합니다.

 

즉 클로저에서 뭔가 작업을 해서 nil이면 downstream에 전달하지 않는 그런 녀석 같네요.

compactMap(_:)

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

정의를 보면 Upstream에서 받은 값을 클로저로 처리된 값이 nil이 아닌 값만 Downstream으로 내려보내는 Operator인 듯하네요.

 

바로 사용해볼게요.

let stringPublisher = ["1", "2", "a.b", "3"].publisher
stringPublisher
    .compactMap { element in
        return Int(element)
    }
    .sink(receiveValue: { print($0) })

위 코드를 보면 String Publisher를 만들어서 각각의 값들을 Int로 변환을 시도하는 것을 볼 수 있습니다. Int로 변환이 안 되는 String 값의 경우엔 nil이 반환되니까 아까 CompactMap의 정의대로라면 그런 값은 downstream으로 전달되지 못하겠네요.

 

실행해보면 결과는 다음과 같이 나옵니다.

CompactMap 예제 코드
1
2
3

오~ downstream으로 전달된 값 중에는 "a.b"라는 String 값이 없는 것을 볼 수 있어요.

정의대로 잘 동작하는 것을 알 수 있었습니다.

TryCompactMap

얘도 뭐 이름을 보니 CompactMap이랑 똑같은데 에러만 던지는 Publisher일 거 같네요.

역시나 정의를 보니 그렇습니다.

tryCompactMap(_:)

TryCompactMap Publisher를 활용해서 만들어진 Operator는 tryCompactMap(_:)입니다.

'

정의를 보면 역시나 compactMap(_:)과 똑같은데 에러만 던질 수 있다고 되어있습니다.

 

그럼 바로 사용해볼게요.

enum PinguError: Error {
    case thisIsNil
}

func checkTranformIntAvailable(string: String) throws -> Int {
    guard let intValue = Int(string) else {
        throw PinguError.thisIsNil
    }
    return intValue
}

let stringPublisher = ["1", "2", "a.b", "3"].publisher
stringPublisher
    .tryCompactMap { element in
        try checkTranformIntAvailable(string: element)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("모두 Int로 변환가능해요")
        }
    }, receiveValue: { print($0) })

간단하게 아까 전에 봤던 compactMap 예제 코드에 nil이 나오면 이번에는 에러를 던지게 코드를 작성했습니다.

결과를 보면 예상대로 아래와 같습니다.

TryCompactMap 예제 코드
1
2
The operation couldn’t be completed. (__lldb_expr_15.(unknown context at $107d139dc).(unknown context at $107d13a1c).(unknown context at $107d13a24).PinguError error 0.)

"a.b"라는 녀석은 Int로 변환을 못해서 에러를 던지는 것을 볼 수 있네요.

RemoveDuplicates

이번에는 RemoveDuplicates라는 Publisher를 살펴보겠습니다.

이름을 보니 뭔가 중복을 제거해줄 것만 같은 느낌이 드네요.

정의를 보면 이전에 내보낸 값과 일치하지 않는 값만 downstream에 전달하는 publisher라고 되어있습니다.

이게 조금 헷갈릴 수 있는데, 바로 직전에 내보낸 값과 일치하는지만 확인해주는 건데요..

이를 활용해서 만들어진 Operator를 보면 이해가 잘 됩니다.

removeDuplicates()

RemoveDuplicates Publisher를 활용해서 만들어진 Operator는 2개인데 그중 하나는 removeDuplicates()입니다.

정의를 보면 Upstream에서 받은 바로 직전의 값과 같지 않은 값을 Downstream으로 내려보낸다고 되어있습니다.

물론 같은지 확인을 해야 하니 Output은 Equatable 프로토콜을 준수하는 타입이어야 합니다.

 

바로 사용해보면 아래와 같이 사용할 수 있어요.

let intPublisher = [1, 2, 3, 1, 1, 2, 2, 3].publisher
intPublisher
    .removeDuplicates()
    .sink(receiveValue: { print($0) })

결과를 보면 아래와 같습니다.

RemoveDuplicates 예제 코드
1
2
3
1
2
3

즉 위 코드를 보면 [1, 2, 3, 1, 1, 2, 2, 3]를 publish 하는데요, 여기서 Set과 같이 [1, 2, 3]만 남는 게 아니고 바로 직전의 값과 동일한 값만 필터링돼서 사라집니다.

위 코드에서는 5번째 1과 7번째 2만 사라진 것을 볼 수 있어요.

 

근데 RemoveDuplicates Publisher의 구현을 살펴보면..

public struct RemoveDuplicates<Upstream> : Publisher where Upstream : Publisher {
    public typealias Output = Upstream.Output

    public typealias Failure = Upstream.Failure

    public let upstream: Upstream

    public let predicate: (Publishers.RemoveDuplicates<Upstream>.Output, Publishers.RemoveDuplicates<Upstream>.Output) -> Bool

    public init(upstream: Upstream, predicate: @escaping (Publishers.RemoveDuplicates<Upstream>.Output, Publishers.RemoveDuplicates<Upstream>.Output) -> Bool)

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

이렇게 되어있는데요, predicate라는 프로퍼티가 있습니다.

얘는 개발자가 직접 원하는 대로 중복 값을 처리할 수 있도록 클로저를 구현하게 해 주는데요, 클로저에서 true를 반환하면 중복으로 처리해서 downstream으로 전달하지 않고 false를 반환해야 중복이 아니라고 판단해서 downstream으로 전달하게 됩니다.

 

이걸 사용하는 방법은 removeDuplicates(by:)를 사용하면 되는데요, 바로 알아보겠습니다.

removeDuplicates(by:)

removeDuplicates(by:)의 정의는 위와 같습니다. Upstream에서 바로 직전에 받은 값을 제공된 클로저로 현재 값과 같은지 비교해서 Downstream으로 내려보낸다고 되어있습니다.

 

또한 비교 로직을 직접 구현할 수 있어서 아까 전의 removeDuplicates() 다르게 Output이 Equatable 프로토콜을 준수하지 않아도 됩니다.

 

실제로 사용해보면 아래와 같습니다.

struct Name {
    let lastName: String
    let firstName: String

    func printName() {
        print(lastName + firstName)
    }
}

let namePublisher = [
    Name(lastName: "Pin", firstName: "gu"),
    Name(lastName: "Pin", firstName: "ga"),
    Name(lastName: "Ro", firstName: "By"),
    Name(lastName: "O", firstName: "dung")
].publisher

namePublisher
    .removeDuplicates(by: { prev, current in
        return prev.lastName == current.lastName
    })
    .sink(receiveValue: { $0.printName() })

실행해보면 결과는 아래와 같아요.

RemoveDuplicates(by:) 예제 코드
Pingu
RoBy
Odung

성과 이름을 저장해주는 Name이라는 구조체를 만들었습니다.

그리고 removeDublicates의 중복확인 로직으로 바로 직전 값과 lastName이 같으면 중복으로 처리하도록 구현했습니다.

그러면 위와 같이 "Pin"이라는 lastName이 연속으로 나와서 뒤에 publish 된 "Pin ga"는 downstream으로 전달되지 못한 것을 볼 수 이 있습니다.

TryRemoveDuplicates

이제 Try만 붙어도 에러를 내보내는 기능만 추가된 Publisher라는 걸 알 수 있겠네요.😄

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

tryRemoveDuplicates(by:)

TryRemoveDuplicates Publisher를 활용해서 만들어진 tryRemoveDuplicates(by:)의 정의를 봐도 아까 알아본 removeDuplicates(by:)와 동일하지만 에러만 던질 수 있다는 차이가 있네요.

 

바로 사용해보면 아래와 같이 사용할 수 있겠네요.

enum PinguError: Error {
    case duplicate
}

let intPublisher = [1, 2, 2, 3, 3].publisher
intPublisher
    .tryRemoveDuplicates { prev, current in
        if prev == current {
            throw PinguError.duplicate
        }
        return prev == current
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("중복값 없음")
        }
    }, receiveValue: { print($0) })

이번에는 현재 값을 바로 직전 값과 비교했을 때 같다면 downstream으로 내려보내지 않을 뿐 아니라 에러를 던지도록 만들었습니다.

그리고 결과는 아래와 같아요.

TryRemoveDuplicates 예제 코드
1
2
The operation couldn’t be completed. (__lldb_expr_47.(unknown context at $100fcd214).(unknown context at $100fcd2b4).(unknown context at $100fcd2bc).PinguError error 0.)

ReplaceEmpty

다음은 ReplaceEmpty입니다. 뭔가 비어있는 것을 대체하는 역할을 할 거 같은데, 맞는지 알아볼게요.

예상대로 빈 스트림을 전달받으면 제공된 element로 바꿔주는 역할을 하는 Publisher라고 합니다.

replaceEmpty(with:)

ReplaceEmpty Publisher를 활용해서 만들어진 replaceEmpty(with:)의 정의를 보면 간단하게 Upstream에서 빈 stream을 받으면 매개변수로 받은 값으로 대체해서 Downstream으로 보내는 역할을 한다고 합니다.

 

간단하게 사용해보면 아래와 같습니다.

let intPublisher = [].publisher
intPublisher
    .replaceEmpty(with: 5)
    .sink(receiveValue: { print($0) })

빈 배열이 전달되면 5로 바꿔주도록 만들었습니다.

그리고 결과는 당연히..

ReplaceEmpty 예제 코드
5

ReplaceError

이제 오늘 알아볼 마지막 Publisher인 ReplaceError를 알아보겠습니다.

이름만 보면 에러를 원하는 값으로 바꿔줄 거 같네요.

그리고 실제 정의를 봐도 그렇습니다!

replaceError(with:)

ReplaceError Publisher를 활용해서 만들어진 Operator는 replaceError(with:)입니다.

정의를 보면 에러를 받으면 매개변수로 받은 값으로 바꿔주는 역할을 한다고 합니다.

 

그럼 아까 해본 예제 중에서 String을 Int로 바꾸던 예제를 다시 활용해서 ReplaceError를 사용하는 예제를 만들어볼게요.

enum PinguError: Error {
    case thisIsNil
}

func checkTranformIntAvailable(string: String) throws -> Int {
    guard let intValue = Int(string) else {
        throw PinguError.thisIsNil
    }
    return intValue
}

let stringPublisher = ["1", "2", "a.b", "3"].publisher
stringPublisher
    .tryCompactMap { element in
        try checkTranformIntAvailable(string: element)
    }
    // Error를 0으로 모두 대체
    .replaceError(with: 0)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("모두 Int로 변환가능해요")
        }
    }, receiveValue: { print($0) })

결과는 아래와 같아요.

ReplaceError 예제 코드
1
2
0
모두 Int로 변환가능해요

"a.b"를 Int로 변환하려고 하면 에러를 발생시켰는데, ReplaceError를 사용해서 에러가 발생할 경우 0으로 바꿔주도록 했더니 completion도 finished로 처리된 것을 볼 수 있습니다.

 

공식문서에 따르면 replaceError는 하나의 Element를 보내고 스트림을 종료해서 에러를 처리하려는 경우에 유용하다고 하네요. catch라는 Operator를 사용해서 에러를 처리해주는 게 좋다고 합니다.

 

마침 위 예제가 여러 개의 Element를 처리하고 있으니 간단하게 catch로도 처리해볼게요.

enum PinguError: Error {
    case thisIsNil
}

func checkTranformIntAvailable(string: String) throws -> Int {
    guard let intValue = Int(string) else {
        throw PinguError.thisIsNil
    }
    return intValue
}

let stringPublisher = ["1", "2", "a.b", "3"].publisher
stringPublisher
    .tryCompactMap { element in
        try checkTranformIntAvailable(string: element)
    }
    // Error를 0으로 모두 대체
    .catch { error in
        Just(0)
    }
    .sink(receiveCompletion: { completion in
        switch completion {
        case .failure(let error):
            print(error.localizedDescription)
        case .finished:
            print("모두 Int로 변환가능해요")
        }
    }, receiveValue: { print($0) })

결과는 아까와 동일합니다.

ReplaceError With Catch 예제 코드
1
2
0
모두 Int로 변환가능해요

 

이렇게 Combine의 Operator 중 Filtering Operator로 분류된 것들에 대해 알아봤습니다.

다음 글에서는 Reducing Elements를 알아보도록 하겠습니다.

 

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

 

감사합니다~!

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함