이전까지 Publisher 와 Subscriber 이제 연산자를 알아보자. 가장 간단한 연산자로는 Sink(읽기) 와 Assign(쓰기) 이 있다.sink 연산자(operator)는 두 개의 컴플리션 핸들러를 가지고 있다. 하나는 값을 받는 receiveValue() 메서드, 하나는 구독이 끝났을 때 호출 되는 receiveCompletion() 메서드이다. 위 예제에서 사용된 Just는 Publisher의 한 종류로 단 하나의 값만을 방출하고, 그 후에는 완료 이벤트를 보낸다.
assign(to:on:) 연산자는 특정 객체의 프로퍼티에 값을 자동으로 할당하는 데 사용된다. 내부를 들여다보면 ReferenceWritableKeyPath 타입을 통해 추상화된 프로퍼티 위치를 이용해 접근해서 쓰기 작업을 한다. assign 연산자는 특히 데이터 바인딩에서 유용하게 쓸 수 있다. (주의할 점은 assign연산자는 쓰기 작업만 한다. 읽기 작업은 하지 않는다.)
import Combine
import UIKit
class ViewModel {
@Published var userName = "User"
}
class ViewController: UIViewController {
var viewModel = ViewModel()
private var cancellables = Set<AnyCancellable>()
@IBOutlet weak var nameLabel: UILabel!
override func viewDidLoad() {
super.viewDidLoad()
viewModel.$userName
.receive(on: RunLoop.main)
.assign(to: \.text!, on: nameLabel)
.store(in: &cancellables)
}
}
지금까지 Publisher(이벤트 발행자), Subscriber(이벤트 구독자), Operator(연산자) 에 대해 간단히 알아보았다. 다음으로 Subject 에 대해 알아보자. Subject는 Publisher(이벤트 발행자)이자 Subscriber(구독자)인 자웅동체(?)이다. 외부에서 이벤트를 수신하고, 이를 구독하고 있는 다른 구독자들에게 방출할 수 있습니다. 이는 다른 구성 요소들과의 데이터 통신에 매우 유용하며, 특히 동적 이벤트나 사용자 상호작용을 처리할 때 자주 사용된다.
Combine에는 주로 두 가지 기본 Subject가 있다. 바로 PassThroughSubject와 CurrentValueSubject 이다.
PassthroughSubject 는 Subject는 값을 저장하지 않고, 새로운 값을 받을 때마다 구독자에게 그 값을 즉시 전달한다. PassthroughSubject는 이벤트 스트림이나 상태 변화를 바로 구독자에게 전달하고 싶을 때 사용한다.
CurrentValueSubject 는 Subject는 현재의 값을 유지하며, 새 구독자가 생성될 때 현재 값을 즉시 해당 구독자에게 전달한다. 그리고 새로운 값이 입력될 때마다 그 값을 업데이트하고 구독자들에게 방출한다. OOP의 옵저버 패턴과 다르게 구독전 이벤트/값을 가져올 수 있다는 것이 특징이다.
import Combine
import Foundation
// Codable 구조체 정의
struct WeatherResponse: Codable {
let location: Location
let current: CurrentWeather
}
struct Location: Codable {
let name: String
let region: String
let country: String
let lat: Double
let lon: Double
let tz_id: String
let localtime: String
}
struct CurrentWeather: Codable {
let temp_c: Double
let condition: WeatherCondition
}
struct WeatherCondition: Codable {
let text: String
let icon: String
let code: Int
}
// 날씨 데이터를 제공하는 서비스 인터페이스
protocol WeatherService {
func fetchWeather(city: String) -> AnyPublisher<String, Never>
}
// 첫 번째 구현: API로부터 날씨 데이터를 받는 서비스
class APIWeatherService: WeatherService {
private let apiKey = "비밀"
private var cancellables = Set<AnyCancellable>()
deinit { cancellables.forEach { $0.cancel() } }
func fetchWeather(city: String) -> AnyPublisher<String, Never> {
let urlString = "https://api.weatherapi.com/v1/current.json?key=\(apiKey)&q=\(city)&aqi=no"
guard let url = URL(string: urlString) else {
return Just("Invalid URL").eraseToAnyPublisher()
}
let publisher = URLSession.shared.dataTaskPublisher(for: url)
.map(\.data)
.decode(type: WeatherResponse.self, decoder: JSONDecoder())
.map { response -> String in
"Weather in \(response.location.name): \(response.current.condition.text)"
}
.replaceError(with: "Failed to fetch weather")
.eraseToAnyPublisher()
publisher
.receive(on: RunLoop.main)
.sink(receiveValue: { _ in })
.store(in: &cancellables)
return publisher
}
}
// 두 번째 구현: 로컬 데이터베이스에서 날씨 데이터를 제공
class LocalWeatherService: WeatherService {
private var cancellables = Set<AnyCancellable>()
deinit { cancellables.forEach { $0.cancel() } }
func fetchWeather(city: String) -> AnyPublisher<String, Never> {
let localData = "Local Weather Data for \(city): Sunny"
let publisher = Just(localData)
.eraseToAnyPublisher()
publisher
.receive(on: RunLoop.main)
.sink(receiveValue: { _ in })
.store(in: &cancellables)
return publisher
}
}
// 클라이언트 코드
func printWeatherData(weatherService: WeatherService) {
weatherService.fetchWeather(city: "London")
.sink(receiveValue: { print($0) })
.store(in: &subscriptions)
}
var subscriptions = Set<AnyCancellable>()
let apiService = APIWeatherService()
let localService = LocalWeatherService()
printWeatherData(weatherService: apiService)
printWeatherData(weatherService: localService)
지금까지 알아봤듯이 Publisher 와 Subject는 각각의 세부타입이 있었다. 만약 하는 역할이 같은데 다른 Publisher나 Subject 세부타입으로 구현해주어야한다면 어떻게 추상화를 시킬 수 있을까? 바로 eraseToAnyPublisher 메서드를 통해 타입을 지울 수 있다. 위 코드에서 보면 알 수 있듯이 eraseToAnyPublisher() 는 반환 Publisher 타입을 지워주어 AnyPublisher 를 반환한다. 이것이 왜 중요한가는 인터페이스를 만드는데 있어 다형성을 구현할 수 있기 때문이다!! 위 코드에서 알 수 있듯이 DB에서 날씨를 가져오든 Network로 날씨를 가져오든 하나의 인터페이스 메서드로 다양한 Publisher메서드를 구현체로 만들 수 있다!
https://www.kodeco.com/5429795-reactive-programming-in-ios-with-combine/lessons/3
'Flutter' 카테고리의 다른 글
UIView 의 Drawing Cycle (0) | 2024.05.08 |
---|---|
| Combine | 3. Transforming Operators (0) | 2024.05.08 |
ViewController의 생명주기 (0) | 2024.05.06 |
| Combine | 1. Getting Started (0) | 2024.05.05 |
UserDefaults 를 사용할 때 객체에 Codable 을 채택한 이유 (0) | 2024.05.05 |