본문 바로가기
Swift

[Swift] Combine 알아보기[2] @Published (Publisher), Operator, Scheduler

by ykr0919 2024. 3. 17.

 

https://h2kangrok.tistory.com/31

 

[Swift] Combine 알아보기 [1] Publisher, Subscriber

오늘은 Combine이다! 처음에 Combine을 들었을 때 이거만 생각남 ㅋㅋ 이제는 iOS 공부를 하는 사람으로서 Swift Combine을 먼저 떠올려야 한다!! 💡 Combine의 모든 내용을 다룰 수는 없어서 간단하게 보자

h2kangrok.tistory.com

 

이전글 이어서 Combine에 대해서 계속 알아보자!! 🔥

 

 

@Published (Publisher)

 

@Published로 선언된 프로퍼티를 퍼블리셔로 만들어줌. 

클래스에 한해서 사용됨 (구조체에서 사용 안됨)

$를 이용해서 퍼블리셔에 접근할 수 있음. 

 

예시를 한번 볼까?? 

 

class Weather {
    @Published var temperature: Double
    init(temperature: Double) {
        self.temperature = temperature
    }
}

let weather = Weather(temperature: 20)
let subscription = weather.$temperature.sink {
    print("Temperature now: \($0)")
}
weather.temperature = 25

// Temperature now: 20.0
// Temperature now: 25.0

 

Weather라는 클래스에 temperature 프로퍼티에 @Published를 붙여 속성 게시 유형으로 만듦.

그 후 weather 인스턴스를 만들어 주었음.

Subcribersink 메서드를 이용해서 구현해 줌. 이제 계속 데이터를 받아 볼 거임 

현재 temperature를 print 해주는데, 처음에는 20이 print 되지만, 값 변경이 일어나니깐 변경된 25도 print 해주고 있음.

temperature값이 변경될 때마다 print 함

 

또 다른 예제도 살펴보면

 

import Foundation
import UIKit
import Combine

final class SomeViewModel {
    @Published var name: String = "Jack"
}

final class Label {
    var text: String = ""
}

let label = Label()
let vm = SomeViewModel()
print("text: \(label.text)")

vm.$name.assign(to: \.text, on: label)
print("text: \(label.text)")

vm.name = "Jason"
print("text: \(label.text)")

vm.name = "Hoo"
print("text: \(label.text)")

//text: 
//text: Jack
//text: Jason
//text: Hoo

 

SomeViewModel이라는 클래스에 name 프로퍼티에 @Published를 붙여 속성 게시 유형으로 만듦.

@Published 속성 래퍼로 선언하면 해당 속성에 대한 변경 사항이 발생할 때마다 자동으로 게시됨.

Label 클래스에 text 프로퍼티를 만들어줌.

 

SomeViewModel name에 $붙은 vm.$name은 이제부터 퍼블리셔가 되는 거임 

이제 이걸 Subscribe를 할 수 있는데 

vm.$name.assign(to: \.text, on: label)

 

퍼블리셔에서 생긴 데이터를 label의 text에 할당을 해줌. 그럼 name이 변경될 때마다 text의 값이 업데이트가 될 거임!!

 

맨 처음 label.text를 출력해 보면 emty String이 출력될 것임! 처음 text의 값! 

 

그 후 SomeViewModel에서 Jack이라는 데이터를 퍼블리셔로 만들어서 Subscriber로 할당

Jack이 출력이 되고 

SomeViewModel의 값을 변경되면  text값이 업데이트됨 

 

 SomeViewModel이 가지고 있는 데이터가 업데이트될 때, Label로 업데이트해야 하는데  업데이트가 되는 데이터를 퍼블리셔로 만들어서 데이터가 변경될 때마다 Label에 전달해 줘!! 라고한번 설정해 주면 값이 계속 변경될 때 label이 업데이트가 되는 걸 볼 수 있는 예제임 

 

 

Operator

Publisher에게 받은 값을 가공해서 Subscriber에게 제공 

Input, Output, Failure type을 받는데 타입이 다를 수 있음

빌트인(Built-in)  Operator가 많이 있음 

  •  map, filter, reduce, collect, combineLatest .. 

 

OperatorPublisher와 Subscriber 사이에서 값들을 Input으로 받아서 Output을 변경시켜 준다던지 or 가공을 한다던지 중간 역할 자를 하고 있음!!

 

빌트인(Built-in)  Operator들 중mapfilter만 간단하게 사용해 보자! 

 

 

map제공된 클로저를 사용하여 업스트림 게시자의 모든 요소를 ​​변환해 줌!!  array에서 시용하던 map이랑 같음!! 

 

import Foundation
import Combine

// Transform - Map

let numPublisher = PassthroughSubject<Int, Never>()
let subscription = numPublisher
    .map{ $0 * 2}
    .sink{ value in
            print("Transformed Value: \(value)")
}

numPublisher.send(10)
numPublisher.send(20)
numPublisher.send(30)
subscription.cancel()

//Transformed Value: 20
//Transformed Value: 40
//Transformed Value: 60

 

위 코드에서는 데이터가 들어올 때 map에서 값을 두배로 만들어 주고 있음! 

 

 

filter제공된 클로저와 일치하는 모든 요소를 ​​다시 게시해 줌 

 

import Foundation
import Combine

// Filter
let stringPublisher = PassthroughSubject<String, Never>()
let subscription = stringPublisher
    .filter { $0.contains("a")}
    .sink { value in
            print("Filtered Value: \(value)")
}
stringPublisher.send("abc")
stringPublisher.send("Jack")
stringPublisher.send("Joon")
stringPublisher.send("Jenny")
stringPublisher.send("Jason")
subscription.cancel()

//Filtered Value: abc
//Filtered Value: Jack
//Filtered Value: Jason

 

Publisher에서 이벤트를 받을 때  filter를 시킬 것임 

String데이터에 "a"가 포함되어 있는 값만 보내고 나머지는 보내지 않음!! 

 

요약하자면 ~!! 

 

map은 넘겨받은 데이터를 가공을 해서 넘겨주는 것이고!!  filter는 넘겨받은 데이터에서 조건을 만족하는 데이터만 넘겨주는 것임 !~

빌트인(Built-in)  Operator가 많이 있는데 그중에서 2개만 간단하게 알아봄! 

Operator에 대해서는 나중에 더 자세히 알아볼 거임!! 

 

 

Scheduler 

Scheduler는 언제, 어떻게 클로져를 실행할지 정해주는 녀석임

Operator에서 Scheduler를 파라미터로 받을 때가 있음

  • 따라서, 작업에 따라서, 백그라운드 혹은 메인스레드에서 작업이 실행될 수 있게 해 줌 

Scheduler가 스레드 자체는 아님!!! 

 

2가지 Scheduler 메서드 

 

subscribe(on:)을 이용해서, publisher가 어느 스레드에서 수행할지 결정해 주는 것 

무거운 작업은 메인스레드가 아닌 다른 스레드에서 작업할  수 있게 도와줌 

  • 백그라운드 계산이 많이 필요한 것 
  • 파일 다운로드해야 하는 경우 

 

receive (on:)을 이용해서 operator, subscriber가 어느 스레드에서 수행할지 결정해 주는 것 

UI 업데이트 필요한 데이터를 메인스레드에서 받을 수 있게 도와줌

  • 예) 서버에서 가져온 데이터를 UI 업데이트할 때

 

 Pattern (일반적인 패턴)

let jsonPublisher = MyJSONLoaderPublisher() // Some publisher.

jsonPublisher
    .subscribe(on: backgroundQueue)
    .receive(on: RunLoop.main)
    .sink { value in
		label.text = value
}

 

UI 업데이트 시

 

❌ 이렇게 하지 말고

pub.sink {
    DispatchQueue.main.async {
        // Do update ui
    }
}

 

✅ 이렇게 하기

pub.receive(on: DispatchQueue.main).sink {
        // Do update ui
}

 

자! 예시를 봐야지 이해가 가겠지! 

 

import Foundation
import Combine

let arrPublisher = [1,2,3].publisher

let queue = DispatchQueue(label: "custom")

let subscription = arrPublisher
    .map { value -> Int in
        print("transform: \(value), thread: \(Thread.current)")
        return value
    }
    .sink { value in
    print("Receive value: \(value), thread: \(Thread.current)")
}

//transform: 1, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}
//transform: 2, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}
//transform: 3, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}
//Receive value: 1, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}
//Receive value: 2, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}
//Receive value: 3, thread: <_NSMainThread: 0x600001704040>{number = 1, name = main}

 

지금 퍼블리셔에 Operator가 있고 Subscriber가 있음 

 

예를 들어 첫 번째 작업이 무거운 작업이라고 했을 때 실제 UI가 돌아가고 있는 main 스레드에서는 돌아가면 안 된다고 했을 때

무거운 작업을 main 스레드가 아닌 custom 스레드에서 돌아가게 하고 싶음 

위 코드에서는  전부 main thread에서 돌어가고 있는 모습 🔼

 

import Foundation
import Combine

let arrPublisher = [1,2,3].publisher

let queue = DispatchQueue(label: "custom")

let subscription = arrPublisher
    .subscribe(on: queue)
    .map { value -> Int in
        print("transform: \(value), thread: \(Thread.current)")
        return value
    }
    .receive(on: DispatchQueue.main)
    .sink { value in
    print("Receive value: \(value), thread: \(Thread.current)")
}

//transform: 1, thread: <NSThread: 0x6000017112c0>{number = 5, name = (null)}
//transform: 2, thread: <NSThread: 0x6000017112c0>{number = 5, name = (null)}
//transform: 3, thread: <NSThread: 0x6000017112c0>{number = 5, name = (null)}
//Receive value: 1, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}
//Receive value: 2, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}
//Receive value: 3, thread: <_NSMainThread: 0x600001708040>{number = 1, name = main}

 

subscribe(on:)을 이용해서, publisher가 custom 스레드에서 수행하라고 결정해 줌 

그다음 작업이 완료되고 완료된 데이터를 UI에서 업데이트하는 경우에는 main 스레드에 올려서 데이터를 받도록 변경할 수 있음  

 

스레드는 main 스레드가 아닌 다른 custom 스레드에서 작업이 됨!

스레드 number가 1이면 main 스레드를 의미하고 다른 번호는 main 스레드가 아님! 

 

이와 같이 Scheduler를 사용하는 이유는 뭘까??  🤔 

 

어떤 작업에서 리소스를 효율적으로 활용할 수 있도록 스위칭을 시켜주는 것임 !! 

서버나 작업이 데이터 작업이 무거운 것들은 백그라운드에서 돌리고, 실제로 완료된 데이터를 가지고 UI 업데이트할 때는 다시 main 스레드로 올려서 받는것이 일반적인 패턴임 !!! 

 

 

 

 

지금까지 Combine -  Publisher, Subscriber, @Published (Publisher), Operator, Scheduler 가볍게 알아봤음 

아까지가 전부였으면 좋겠지만 ㅋㅋ 천만의 말씀 ㅋㅋ 

다음글에는 Combine - ObservableObject / @Published / @ObservedObject를 다루어볼꺼임 !! 

 

 

 

 

 

참고 및 인용 

 

이준원 강사님

https://developer.apple.com/documentation/combine/published

https://developer.apple.com/documentation/combine/just-publisher-operators

https://developer.apple.com/documentation/combine/publisher/filter(_:)

https://developer.apple.com/documentation/combine/scheduler

https://developer.apple.com/documentation/combine/publisher/map(_:)-99evh