Promise pattern in Swift
우리는 Swift를 사용하여 비동기 작업의 결과를 전달받고 싶을 때,
대개 Closure를 이용하여 작업의 결과를 처리한다.
대부분 비동기 작업에 대한 결과를 completion handler로 처리하는 iOS의 특성 때문에 이러한 코드에 직면한다.
struct UserProfile { ... }
func login(id: String, password: String, success: @escaping (String) -> Void, failure: @escaping(Error) -> Void) { ... }
func profile(token: String, success: @escaping (UserProfile) -> Void, failure: @escaping (Error) -> Void) { ... }
login(id: "cleanios", password: "123456", success: { token in
self.profile(token: token, success: { (profile) in
// Handle get profile success
}, failure: { (error) in
// Handle get profile error
})
}) { (error) in
// Handle login error
}
Promise는 이러한 nested closures를 해소할 수 있다.
struct UserProfile { ... }
func login(id: String, password: String) -> Promise<String> { ... }
func profile(token: String) -> Promise<UserProfile> { ... }
login(id: "cleanios", password: "123456")
.then { self.profile(token: $0 ) }
.then { // Handle get profile success }
.catch { // Handle error }
위의 두 코드는 같은 동작을 하지만 readability 측면에서 후자가 더 좋은 코드인 것을 알 수 있다.
이처럼 Promise는 데이터와 예외처리를 하나의 흐름 안에서 제어할 수 있도록 해준다.
Promise pattern
혹자는 promise pattern이라 부르는 Promise 문법이 우리에겐 낯설기 때문에 URLSession을 통해 다운로드받은 이미지를 UIImageView에 setup하는 다음과 같은 코드를 promise pattern으로 변환하면서 골자를 익혀보자.
func setupImageView() {
guard let url = URL(string: "https://www.navercorp.com/img/ko/header_logo.png") else { return }
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
print(error)
} else if let data = data, let image = UIImage(data: data) {
DispatchQueue.main.async {
self.imageView.image = image
}
}
}
}
setupImageView()가 너무 길기 때문에 분리하면 다음과 같다.
func setupImageView() {
guard let url = URL(string: "https://www.navercorp.com/img/ko/header_logo.png") else { return }
fetch(from: url) { data, _, error in
if let error = error {
print(error)
} else if let data = data {
self.updateImageView(with: data)
}
}
}
func fetch(from url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completionHandler).resume()
}
func updateImageView(with data: Data) {
guard let image = UIImage(data: data) else { return }
DispatchQueue.main.async {
self.imageView.image = image
}
}
fetch(from:completionHandler:)에서 파라미터로 받던 completion handler를 제거하고, 비동기 작업의 최종 결과인 Promise 객체를 리턴하도록 수정한다.
기존에 completion handler를 파라미터로 받던 함수:
func fetch(from url: URL, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) {
URLSession.shared.dataTask(with: url, completionHandler: completionHandler).resume()
}
Promise 객체를 리턴하도록 수정한 함수:
import Promises
func fetch(from url: URL) -> Promise<Data> {
// Promises의 Promise는 2개의 callback(fulfill, reject)을 가지는 Closure
return Promise<Data> { fulfill, reject in
URLSession.shared.dataTask(with: url) { data, _, error in
if let error = error {
reject(error)
} else if let data = data {
fulfill(data)
}
}.resume()
}
}
import PromiseKit
func fetch(from url: URL) -> Promise<Data> {
// PromiseKit의 Promise는 1개의 callback(seal)을 가지는 Closure
return Promise<Data> { seal in
URLSession.shared.dataTask(with: url) { data, _, error in
seal.resolve(data, error)
}.resume()
}
}
그다음 수정된 fetch(from:)에서 리턴된 Promise 객체로 Promise chaining을 하면 된다.
Promise chaining:
import Promises
func setupImageView() {
guard let url = URL(string: "https://www.navercorp.com/img/ko/header_logo.png") else { return }
fetch(from: url)
.then { self.updateImageView(with: $0) }
.catch { print($0) }
}
func fetch(from url: URL) -> Promise<Data> { ... }
func updateImageView(with data: Data) { ... }
import PromiseKit
func setupImageView() {
guard let url = URL(string: "https://www.navercorp.com/img/ko/header_logo.png") else { return }
firstly {
fetch(from: url)
}.done {
self.updateImageView(with: $0)
}.catch {
print($0)
}
}
func fetch(from url: URL) -> Promise<Data> { ... }
func updateImageView(with data: Data) { ... }
Promises, PromiseKit 등의 Promise를 활용하는 라이브러리별로 세부 문법이 조금씩 상이하기 때문에 이 골자 이상의 자세한 설명은 큰 의미가 없을 것 같다.
Promise 관련 라이브러리들은 좋은 비동기 코드를 위한 더 많은 기능이 있고, 각 라이브러리 GitHub의 documentation에 더 명확하고 자세하게 제공되어 있다.
Promise와 관련된 대부분의 article들이 앞의 예제처럼 Promises나 PromiseKit을 이용한 간략한 예제와 사용법을 다루며, ReactiveSwift 같은 반응형 프로그래밍 라이브러리로도 Promise Class를 구현하여 promise pattern을 적용하는 article도 있다.
이 글의 핵심은 Promises나 PromiseKit의 사용법을 설명하는 것이 아니라 completion handler에 기인한 nested code를 읽기 좋은 코드로 만드는 데 이 개념을 이용하자는 것이다.
좋은 비동기 코드가 필요하다면 자신의 환경과 취향에 맞게 스스로 판단해서 promise pattern을 적용하면 된다.
google/promises의 documentation에서 간략한 환경별 benchmark를 제공한다.
Conclusion
정리하자면 promise pattern은 비동기 작업의 최종 결과인 약속(Promise)으로 데이터와 예외처리를 하나의 흐름 안에서 제어하여 비동기 코드의 명확성과 가독성을 높인다.
Promise 관련 라이브러리를 사용하면 확실히 적은 학습 비용과 적은 코드 변경으로 유연한 비동기 코드를 작성할 수 있다.
내가 Promise를 소개하고 싶었던 이유는 Promise를 활용하면 이런게 좋아!도 있겠지만, 무심코 지나쳐 버릴 수 있는 중첩된 비동기 코드를 누군가는 가독성을 높이기 위해 고민했다는 것과 그 흔적을 통해서 여러분이 프로그래머로서 영감과 자극을 받길 원하기 때문이다.
Related Articles
Links
Promise pattern in Swift
우리는 Swift를 사용하여 비동기 작업의 결과를 전달받고 싶을 때,
대개 Closure를 이용하여 작업의 결과를 처리한다.
대부분 비동기 작업에 대한 결과를 completion handler로 처리하는 iOS의 특성 때문에 이러한 코드에 직면한다.
Promise는 이러한 nested closures를 해소할 수 있다.
위의 두 코드는 같은 동작을 하지만 readability 측면에서 후자가 더 좋은 코드인 것을 알 수 있다.
이처럼 Promise는 데이터와 예외처리를 하나의 흐름 안에서 제어할 수 있도록 해준다.
Promise pattern
혹자는 promise pattern이라 부르는 Promise 문법이 우리에겐 낯설기 때문에 URLSession을 통해 다운로드받은 이미지를 UIImageView에 setup하는 다음과 같은 코드를 promise pattern으로 변환하면서 골자를 익혀보자.
setupImageView()가 너무 길기 때문에 분리하면 다음과 같다.fetch(from:completionHandler:)에서 파라미터로 받던 completion handler를 제거하고, 비동기 작업의 최종 결과인 Promise 객체를 리턴하도록 수정한다.기존에 completion handler를 파라미터로 받던 함수:
Promise 객체를 리턴하도록 수정한 함수:
그다음 수정된
fetch(from:)에서 리턴된 Promise 객체로 Promise chaining을 하면 된다.Promise chaining:
Promises, PromiseKit 등의 Promise를 활용하는 라이브러리별로 세부 문법이 조금씩 상이하기 때문에 이 골자 이상의 자세한 설명은 큰 의미가 없을 것 같다.
Promise 관련 라이브러리들은 좋은 비동기 코드를 위한 더 많은 기능이 있고, 각 라이브러리 GitHub의 documentation에 더 명확하고 자세하게 제공되어 있다.
Promise와 관련된 대부분의 article들이 앞의 예제처럼 Promises나 PromiseKit을 이용한 간략한 예제와 사용법을 다루며, ReactiveSwift 같은 반응형 프로그래밍 라이브러리로도 Promise Class를 구현하여 promise pattern을 적용하는 article도 있다.
이 글의 핵심은 Promises나 PromiseKit의 사용법을 설명하는 것이 아니라 completion handler에 기인한 nested code를 읽기 좋은 코드로 만드는 데 이 개념을 이용하자는 것이다.
좋은 비동기 코드가 필요하다면 자신의 환경과 취향에 맞게 스스로 판단해서 promise pattern을 적용하면 된다.
google/promises의 documentation에서 간략한 환경별 benchmark를 제공한다.
Conclusion
정리하자면 promise pattern은 비동기 작업의 최종 결과인 약속(Promise)으로 데이터와 예외처리를 하나의 흐름 안에서 제어하여 비동기 코드의 명확성과 가독성을 높인다.
Promise 관련 라이브러리를 사용하면 확실히 적은 학습 비용과 적은 코드 변경으로 유연한 비동기 코드를 작성할 수 있다.
내가 Promise를 소개하고 싶었던 이유는
Promise를 활용하면 이런게 좋아!도 있겠지만, 무심코 지나쳐 버릴 수 있는 중첩된 비동기 코드를 누군가는 가독성을 높이기 위해 고민했다는 것과 그 흔적을 통해서 여러분이 프로그래머로서 영감과 자극을 받길 원하기 때문이다.Related Articles
Links