티스토리 뷰

반응형

 

안녕하세요 :) Zedd입니다.

오늘은..8번째 시리즈..

Combining Elements from Multiple Publishers

입니다.

 

combineLatest

merge(with:)

zip

 

봐야할 것은 이 3개입니다.

이 전 시리즈들을 보시려면 여기를 참고해주세요!

 

combineLatest


combineLatest는 additional publisher를 구독하고,

두 게시자로부터 ouput을 수신하면 클로저를 호출하는 친구입니다.

일단은 

이런식으로 사용할 수 있습니다. 

왜 저런 결과가 나왔는지 하나씩 보도록 합시다.

일단 보기전에, "두 게시자로부터 ouput을 수신하면" 클로저를 호출하는 친구라는 사실을 기억해야합니다.

먼저 combineLatest를 사용한 코드부터 보겠습니다. 

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
    .combineLatest(pub2) { (first, second) in
        return first * second
    }
    .sink { print("Result: \($0).") }

코드는 간단합니다. pub1과 pub2에 들어가는 integer를 곱해서 결과를 내놓습니다.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
    .combineLatest(pub2) { (first, second) in
        return first * second
    }
    .sink { print("Result: \($0).") }

pub1.send(2) // ✔️

pub1에 2를 보냈습니다. 

하지만 아무것도 출력이 안됩니다.

왜냐? combineLatest는 "두 게시자로부터 ouput을 수신하면" 클로저를 호출합니다.

pub1으로부터 값을 받았지만, pub2에게서는 아무값도 못받았으니, 클로저가 실행이 안됩니다.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
    .combineLatest(pub2) { (first, second) in
        return first * second
    }
    .sink { print("Result: \($0).") }

pub1.send(2)
pub2.send(2) // ✔️

// Prints:
//Result: 4.    (pub1 latest = 2, pub2 latest = 2)

자 이제야 비로소 4가 출력이 됩니다.

왜냐면 pub1과 pub2 둘 다에 output이 수신됐기 때문이죠.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
    .combineLatest(pub2) { (first, second) in
        return first * second
    }
    .sink { print("Result: \($0).") }

pub1.send(2) 
pub2.send(2)
pub1.send(9) // ✔️

// Prints:
//Result: 4.    (pub1 latest = 2, pub2 latest = 2)
//Result: 18.   (pub1 latest = 9, pub2 latest = 2)

자 그리고 pub1에 9를 보내겠습니다. 

pub1만 보냈으니 값이 안나올거야! 라고 생각하실수도 있지만..아닙니다.

바로 18이 나오게 됩니다. (2 * 9) 

pub2에는 이전에 우리가 넣어줬던 2가 마지막 값으로 있기 때문입니다. 

(combineLatest는 1의 버퍼크기를 이용하여 각 버퍼에 가장 최근값을 가지고 있는다고 해요.)

그러니까 pub2의 버퍼에는 2가 들어있는겁니다.

그래서 pub1의 9와 pub2의 2가 곱해져서 18의 결과가 나오게 된것이죠.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()
cancellable = pub1
    .combineLatest(pub2) { (first, second) in
        return first * second
    }
    .sink { print("Result: \($0).") }

pub1.send(2)
pub2.send(2)
pub1.send(9)
pub1.send(3)
pub2.send(12)
pub1.send(13)

//
// Prints:
//Result: 4.    (pub1 latest = 2, pub2 latest = 2)
//Result: 18.   (pub1 latest = 9, pub2 latest = 2)
//Result: 6.    (pub1 latest = 3, pub2 latest = 2)
//Result: 36.   (pub1 latest = 3, pub2 latest = 12)
//Result: 156.  (pub1 latest = 13, pub2 latest = 12)

그 뒤로는 다 똑같습니다. 하나하나 해보세요.

combineLatest이름을 그냥 생각하시면 됩니다. 결합하다 + 가장 최근(값이랑) 

 

RxSwift때도 그랬지만..CombineLatest가 Validation체크에 종종 쓰이곤 하는데요

class LoginViewModel: ObservableObject {

    @Published var id: String = ""
    @Published var password: String = ""
    
    var validInfo: AnyPublisher<Bool, Never> {
        return self.$password.combineLatest(self.$id) {
            return $0 == $1
        }.eraseToAnyPublisher()
    }
}

뭐 이런식으로 사용될 수 있겠죠?

놀랍게도 id와 password가 같아야지만 valid하다고 판단하는...코드입니다.

뭐 이 부분은 제가 대충한거라 알아서들 바꾸시면 될 것 같아요. 

struct ContentView: View {
    
    @ObservedObject var viewModel: LoginViewModel = LoginViewModel()
    @State private var isValid = false
    
    var body: some View {
        Form {
            TextField("아이디", text: self.$viewModel.id)
            TextField("비밀번호", text: self.$viewModel.password)
            Button("로그인", action: {
                
            }).disabled(!self.isValid)
        }
        .onReceive(self.viewModel.validInfo,
                   perform: { self.isValid = $0 })
    }
}

View쪽에서는 이런식으로 사용할 수 있습니다. 

class LoginViewModel: ObservableObject {
    @Published var id: String = ""
    @Published var password: String = ""
    
    var validInfo: AnyPublisher<Bool, Never> {
        return Publishers.CombineLatest(self.$id, self.$password).map {
            return $0 == $1
        }.eraseToAnyPublisher()
    }
}

이렇게도 사용할 수 있습니다. 

 

참고로 combienLatest의 publisher가 완료(finish)되려면 모든 업스트림 publisher가 완료되어야합니다.

만약 업스트림이 계속 값을 publish하지 않는다면 이 publisher는 종료가 안됩니다...

역시나 결합된 publisher중 하나라도 실패로 종료되면 이 publisher역시 실패하게 됩니다. 

 

combineLatest는...

6개가;;; 있는데..뭐 별건 아닌것 같고..

아셔야 할 건 그냥 결합할 수 있는 친구들의 개수가 최대 4개입니다.

return self.$password.combineLatest(self.$id, self.$id, self.$id) { (id, password, id2, id3) in
     return id == id2
 }.eraseToAnyPublisher()
return Publishers.CombineLatest3(self.$id, self.$password, self.$id).map { (id, pw, id2) in
     return id == id2
}.eraseToAnyPublisher()

뭐 이런식....CombineLatest4까지 있습니다.

 

merge


merge가 머지...?

ㅋㅋ

웃기죠

(머지가 merge..? 로 응용도 가능)

merge는 말 그대로 합치는겁니다.

publisher의 요소를 동일한 타입의 다른 publisher의 요소와 결합하여

interleave된 요소 시퀀스를 제공하는 친구라고 해요.

그냥 정말 하나의 스트림으로 합쳐주는 역할입니다.

2개의 publisher를 하나의 publisher로 다루고 싶을 때 유용합니다.

이것도 하나씩 살펴보도록 합시다.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

let cancellable = pub1.merge(with: pub2)
    .sink { print("\($0)", terminator: " " )}

pub1.send(2)

// Prints: "2"

combineLatest처럼 뭐 다른 publisher에 값이 와야하는게 아닙니다. 그냥 바로바로 와요.

let pub1 = PassthroughSubject<Int, Never>()
let pub2 = PassthroughSubject<Int, Never>()

let cancellable = pub1.merge(with: pub2)
    .sink { print("\($0)", terminator: " " )}

pub1.send(2)
pub2.send(2)
pub1.send(3)
pub2.send(22)
pub1.send(45)
pub2.send(22)
pub1.send(17)

// Prints: "2 2 3 22 45 22 17"

pub1과 pub2를 merge하는 스트림을 구독하고 있으므로

pub1에 보내든 pub2에 보내든..그냥 다 값을 잘 받습니다.

merged publisher는 모든 업스트림 publisher가 완료(finish)될 때 까지 계속 요소를 내보내며,

업스트림 publisher가 오류를 생성하면 merged publisher가 해당 오류와 함께 실패하게 됩니다.

Merge는 최대 8개까지 할 수 있을 것 같지만..

Publishers.MergeMany라는 것이 존재합니다..

 Publishers.MergeMany(self.$id, self.$password, self.$id, self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id,self.$id).sink { (result) in
    print(result)
}.store(in: &self.cancellable)

대충 이런거 가능

merge를 어떨 때 쓰면 좋냐면..일단 뭔가 응답오는 순서는 상관없고

냥 일괄적으로 api를 요청하고 싶을 수 있습니다.

출처 : stackoverflow.com/a/58708381

이럴 때 merge를 쓰면 좋습니다.

1, 2, 3순으로 요청했다고 응답이 1, 2, 3으로 오는건 아니구요.

실행때마다 달라질 수 있습니다. 

 

zip


zip은 다른 publisher의 요소를 결합하고 요소 쌍을 tuple로 제공하는 친구입니다.

zip는 각각의 publisher가 주는 값을 "결합하는" 친구이기 때문에 

combineLastest처럼 두 publisher모두에게 값이 오지않으면 값을 방출하지 않습니다.

그리고 묶인 publisher는 각 publisher에서 "가장 오래 사용되지 않은" 이벤트를

구독자에게 tuple로 넘겨준다는 사실도 아셔야합니다.

예제로 봅시다. 

numbersPub과 lettersPub을 zip으로 함께 묶었습니다. 

let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()

cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
     
numbersPub.send(1) // ✔️

numbersPub에 값을 보내고, lettersPub에는 값을 안보냈죠. 

두 publisher모두에게 값이 오지않으면 값을 방출하지 않으므로 아무것도 출력되지 않습니다.

let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()

cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
     
numbersPub.send(1)
numbersPub.send(2) // ✔️

역시 lettersPub에는 아직 값이 없으므로 아무것도 출력되지 않습니다.

let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()

cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
     
numbersPub.send(1)
numbersPub.send(2) 
lettersPub.send("A") // ✔️

 // Prints:
 //  (1, "A")

자..lettersPub에도 값이 보내졌으니 두 publisher에 모두 값이 있습니다. 

tuple형식으로 나왔고, (1, A)가 나왔네요. 2는 어디간거죠...? 

아까 

각 publisher에서 "가장 오래 사용되지 않은" 이벤트를 구독자에게 tuple로 넘겨준다..고 그랬었죠?

numbersPub에서 가장 오래 사용되지 않은 값은 1이기 때문에 1을 넘겨준겁니다.

let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()

cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
     
numbersPub.send(1)
numbersPub.send(2) 
lettersPub.send("A") 
numbersPub.send(3) // ✔️

 // Prints:
 //  (1, "A")

자.. numbersPub에 3을 보냈습니다만..값이 나올까요? 

네 나오지 않습니다! 왜냐면 lettersPub에 있는 A는 이미 (1, "A")에서 써버렸거든요.

let numbersPub = PassthroughSubject<Int, Never>()
let lettersPub = PassthroughSubject<String, Never>()

cancellable = numbersPub
     .zip(lettersPub)
     .sink { print("\($0)") }
     
numbersPub.send(1)
numbersPub.send(2) 
lettersPub.send("A") 
numbersPub.send(3) 
lettersPub.send("B") // ✔️

 // Prints:
 //  (1, "A")
 //  (2, "B")

lettersPub에 B를 보냅니다.

numbersPub에서  "가장 오래 사용되지 않은" 값은 2네요?

그럼 (2, "B")를 묶어서 tuple로 나오게 됩니다.

 

아시겠나요?

Zip은 4개까지 할 수 있는 것 같습니다. (ZipMany같은거 없음;;)

그럼 역시나..실제로 어떻게 쓰면 좋을지..생각해봅시다. merge때 썼던..예제를 좀 수정해봤어요. 

api를 호출하고 싶은데 이 비동기 작업이 완료될 때 까지 기다리고 싶다! 하면 zip을 사용하면 간단해집니다.

combineLatest를 사용해도 되겠지만..그냥 어떤 느낌으로 쓰는건지 봐주세요!

 

공부하면서 정말 재밌었네요 XD

틀린 부분을 발견하시면 댓글로 알려주세요!!

반응형