UIKit과 RxSwift를 사용한 토이프로젝트를 마무리하고 이번에는 SwiftUI와 Combine을 주력으로 사용하는 토이프로젝트를 시작해보자 라는 마음을 먹었다. (비록 회사에서 SwiftUI를 쓸 일은 자주 없지만ㅎ) 기술 스택을 서치하던 중, Combine 무용론에 대한 의견을 접하게 되었다. 기존의 나는 background에서 네트워크 통신을 하면서 Combine을 애용해왔기에 충격이 컸다. '나 잘못 쓰고 있었나봐!' 하지만 주니어가 그럴 수도 있지. 서치를 통해 WWDC21에 소개된 async-await를 사용하는 것이 더 적합하다는 의견을 수용하며 내가 정리한 main point는 아래와 같다.
reactive programming framework
https://wwdcbysundell.com/2021/the-future-of-combine/
고마워요 John!
Combine이 쓸모없다는 것이 아니라 본디 이 프레임워크는 비동기 프로그래밍 구현을 목표로 하고 있지 않다는 것을 알게 해준 당신...
나는 문서를 꼼꼼히 읽기보다는 일단 무작정 사용해 보는 것을 선호하는 편인데 이번에는 영어 공부도 할 겸 Swift 5.7 의 concurrency chapter를 신중하게 읽어보았다. 그리고 playground에 샘플을 작성해보았는데 오잉 컴파일 에러?ㅠ 프론트 엔드 개발을 했었던 나로서는 async-await 개념이 낯설지 않았으므로 (javascript 짱) '샘플 구현 쯤이야 개꿀~😸' 이라고 생각했는데 내가 바보여서 놓친 부분이 있었다 헤헤. 이를 포함하여 공식 문서에서 기억하면 좋을만한 점들을 요약해보겠다.
- async 함수/메서드/프로퍼티 안에서
- static main() 메서드, @main으로 마크 된 클래스, enum 안에서
- child task 안에서
이걸 내가 몰랐네! async 함수를 call 하려면 async 함수 안에서 쓰던가 아니면 Task 안에서 써야한다는 것을...
공식문서의 sample을 약간 응용하여 내 기준 좀더 쉽게 작성해보았다. SwiftUI를 사용했고 버튼을 클릭하면 async 작업이 시작되는 간단한 코드이다. 시간이 소요되는 것을 흉내내기 위해 Task.sleep(nanoseconds:)
를 이용하였다.
struct ContentView: View {
var body: some View {
VStack {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
Button("Async Test Start") {
Task.init {
let async = AsyncTest()
await async.test()
print("async test done")
}
}
}
}
}
test() 메서드가 return 될 때까지 task가 suspended 되므로
test 작업을 마친 후에 "test done"이 찍히게 된다.Text("async test")
.task {
let group = TaskGroupTest()
await group.test()
}
하지만 나는 button의 action으로 동작하기 원했기 때문에 action의 closure 안에서 Task를 init하여 사용했다.
class AsyncTest {
func test() async {
let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
let photoNames = await listPhotos(inGallery: "Summer Vacation")
// async let
async let first = downloadPhotos(name: photoNames[0])
async let second = downloadPhotos(name: photoNames[1])
async let third = downloadPhotos(name: photoNames[2])
let photos = await [first, second, third]
show(photos: photos)
}
private func listPhotos(inGallery name: String) async -> [String] {
do {
try await Task.sleep(nanoseconds: 2)
} catch let error {
print(error.localizedDescription)
}
return ["IMG001", "IMG99", "IMG0404"]
}
private func downloadPhotos(name: String) async -> String {
do {
try await Task.sleep(nanoseconds: 3)
} catch let error {
print(error.localizedDescription)
}
return "IMG001"
}
...
}
listPhotos()
, downloadPhotos()
가 시간이 소요되는 작업을 Task.sleep으로 흉내낸 async 메서드들이다. let firstPhoto = await listPhotos(inGallery: "Summer Vacation")[0]
move(firstPhoto, from: "Summer Vacation", to: "Road Trip")
try await
라고 적었을 것이다.async-let
let photoNames = await listPhotos(inGallery: "Summer Vacation")
async let first = downloadPhotos(name: photoNames[0])
async let second = downloadPhotos(name: photoNames[1])
async let third = downloadPhotos(name: photoNames[2])
let photos = await [first, second, third]
show(photos: photos)
async let
이다.async let
을 통해 각 다운로드는 independently 하게 at the same time 에 실행된다.https://docs.swift.org/swift-book/LanguageGuide/Concurrency.html
Swift로 비동기 코드를 구현하는 것은 새삼스러운 일이 아니긴 하지만 과거에 사용했던 기술(?) 보다는 확실히 async-await 가 간단하고 좋다. 하지만 iOS 13+에서 지원하기 때문에 당장 현업에는 적용하기가 어렵게 되었다. 아쉽다. 그래서 토이 프로젝트에서 잘 써먹어 볼 생각이다.