Combine은 Publisher와 Subscriber를 만들어 비동기적 이벤트를 처리하는 프레임워크입니다
Combine이란 시간이 지남에 따라 변화하는 비동기적 이벤트, 데이터에 대응하여 데이터를 처리하는 프레임워크입니다.
앞으로 작성할 Combine 코드에 대해 쉽게 접근하기 위해서는 다음과 같은 순서를 보면서 쉽게 이해할 수 있을 것 같습니다.
1. 어떤 데이터를 구독할 것인지 결정합니다
가장 먼저 어떤 구조의 데이터를 받을 것인지를 결정해야합니다. 아래는 URL로부터 받을 모델을 정의한 코드입니다.
struct PostModel: Identifiable, Codable {
let userId: Int
let id: Int
let title: String
let body: String
}
2. API 데이터를 백그라운드로 받습니다
API로부터 받을 데이터를 정의하였으면 이제 사용자가 데이터를 받기 위해 API의 서비스를 구독해야합니다. 그러기 위해서는 우선 데이터를 받기 위한 구독자 dataTaskPublisher를 만듭니다. 다음으로 publisher가 subscribe(on:_) 메서드를 사용하여 데이터를 백그라운드에서 가져오도록 설정합니다.
백그라운드로 데이터를 받는 이유는 비동기적 작업을 메인 스레드에서 하게 되면 UI 블로킹, UX 저하, 앱 강제 종료 같은 치명적인 문제들이 발생할 수 있기 때문에 백그라운드에서 데이터를 받습니다.
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
3. API로부터 반환받은 데이터를 메인 스레드로 받습니다
백그라운드 스레드를 사용하여 API로부터 가져온 데이터를 메인 스레드로 받습니다. 왜냐면 UI를 업데이트하는 작업은 메인 스레드에서 진행되기 때문입니다. 그렇기 때문에 다음과 같이 receive(on: _) 메서드를 사용하여 메인 스레드에서 데이터를 받도록 정의합니다.
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
.receive(on: DispatchQueue.main)
4. 받은 데이터가 유효한지 확인합니다
받은 데이터가 유효한지 확인하기 위해 tryMap을 사용합니다. tryMap():) 메서드는 upstream publisher로부터 받은 모든 요소들을 변형할 수 있으며 에러를 throw할 수 있습니다. 즉 아래 코드처럼 만약 원하는 값이 아닐 경우에 error를 throw하도록 만들 수 있습니다.
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
guard let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return data
}
5. 데이터가 유효하면 원하는 데이터 타입으로 만들기
만약 데이터가 유효하다면 데이터를 원하는 형태로 만들어야합니다. 그렇게 하기 위해서는 JSON 값을 받기 때문에 JSONDecoder()를 사용하여 [PostModel] 타입으로 데이터를 디코딩하여 받은 데이터를 [PostModel] 타입으로 만듭니다.
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
guard let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return data
}
.decode(type: [PostModel].self, decoder: JSONDecoder())
6. sink를 사용해 데이터를 앱에 적용하기
sink 는 위에 코드들을 통해서 만들어진 데이터를 처리할 수 있도록하는 메서드입니다. sink 메서드를 사용하면 subscriber를 만들게 됩니다. 메서드의 클로저를 통해서 publisher로부터 받은 데이터를 처리할 수 있으며 위에서 [PostModel] 타입의 데이터를 반환받도록 만들어뒀으므로 해당 데이터를 저장합니다.
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
guard let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return data
}
.decode(type: [PostModel].self, decoder: JSONDecoder())
.sink { (completion) in
print("COMPLETION: \(completion)") // 에러가 발생하면 에러가 발생했다고 알려줍니다.
} receiveValue: { [weak self] (returnedPosts) in
self?.posts = returnedPosts
}
7. store를 사용해 AnyCancellable 저장하기
sink는 AnyCancellable 인스턴스를 반환합니다. 이때 AnyCancellable은 Combine의 작업을 취소할 수 있으며 즉 dataTaskPublisher를 통해서 API 데이터를 가져오는 작업을 취소할 수 있습니다. 이 AnyCancellable를 저장하여 작업을 취소하거나 취소한 작업을 재개할 수 있도록 합니다.
func getPosts() {
guard let url = URL(string: "https://jsonplaceholder.typicode.com/posts") else { return }
var cancellables = Set<AnyCancellable>()
URLSession.shared.dataTaskPublisher(for: url)
.subscribe(on: DispatchQueue.global(qos: .background)) // dataTaskPublisher는 백그라운드 스레드에서 실행되므로 굳이 작성할 필요는 없다
.receive(on: DispatchQueue.main)
.tryMap { (data, response) -> Data in
guard let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return data
}
.decode(type: [PostModel].self, decoder: JSONDecoder())
.sink { (completion) in
print("COMPLETION: \(completion)") // 에러가 발생하면 에러가 발생했다고 알려줍니다.
} receiveValue: { [weak self] (returnedPosts) in
self?.posts = returnedPosts
}
.store(in: &cancellables)
}
간결하게 만들기
tryMap 클로저는 다음과 같이 구성되어 있음을 확인할 수 있습니다. 이를 이용하여 위에 정의한 tryMap을 함수로 만들 수 있습니다.
다음은 tryMap을 함수로 만든 코드입니다.
func handleOutput(output: URLSession.DataTaskPublisher.Output) throws -> Data {
guard let response = output.response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
throw URLError(.badServerResponse)
}
return output.data
}
위처럼 함수로 정의하게 되면 다음 코드처럼 간결하게 코드를 작성할 수 있게 됩니다.
URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.tryMap(handleOutput)
.decode(type: [PostModel].self, decoder: JSONDecoder())
.sink { (completion) in
print("COMPLETION: \(completion)")
} receiveValue: { [weak self] (returnedPosts) in
self?.posts = returnedPosts
}
.store(in: &cancellables)
만약 유효하지 않은 데이터를 받았을 때
만약 받은 데이터들 중 에러가 포함된 데이터가 존재한다면 replaceError 메서드를 사용하여 에러가 있는 데이터를 다른 값으로 대체할 수 있습니다.
URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.tryMap(handleOutput)
.decode(type: [PostModel].self, decoder: JSONDecoder())
.replaceError(with: [])
replaceError 메서드를 사용해서 오류 데이터를 처리하게 된다면 sink(receiveCompletion:receiveValue:) 메서드 대신 sink(receiveValue:) 메서드를 사용해서 실패없이 데이터를 처리할 수 있게 됩니다. 그러면 다음 코드처럼 됩니다.
URLSession.shared.dataTaskPublisher(for: url)
.receive(on: DispatchQueue.main)
.tryMap(handleOutput)
.decode(type: [PostModel].self, decoder: JSONDecoder())
.replaceError(with: [])
.sink(receiveValue: { [weak self] (returnedPosts) in
self?.posts = returnedPosts
})
.store(in: &cancellables)
보통 개발자라면 에러를 다루거나 확인해야하는 경우가 많이 때문에 sink(receiveCompletion:receiveValue:)를 더 많이 사용합니다.
Combine을 사용했을 때 장점
Combine을 사용하게 된다면 @escaping 을 사용하지 않아도 된다는 특징이 있습니다. 왼쪽은 @escaping을 사용하여 API를 가져오는 코드이며 오른쪽은 Combine을 사용하여 데이터를 가져오는 코드입니다. 확인했을 때 Combine을 사용한 코드가 더 간결하고 직관적으로 코드를 파악하기 쉽다는 것을 한눈에 확인할 수 있습니다.
'SwiftUI' 카테고리의 다른 글
Custom tab bar (0) | 2024.04.22 |
---|---|
aligmentGuide (0) | 2024.04.21 |
싱글톤 (1) | 2024.04.19 |
NSCache 인터넷으로 가져온 데이터 캐시에 임시 저장하기 (0) | 2024.04.19 |
RotationGesture 제스처로 돌리기 (0) | 2024.04.18 |