[Swift Concurrency] TaskGroup

Woozoo·2023년 4월 20일
0

[Swift Concurrency]

목록 보기
6/13
class TaskGroupBootcampViewModel: ObservableObject {
    @Published var images: [UIImage] = []
    
}

struct TaskGroupBootcamp: View {
    @StateObject private var viewModel = TaskGroupBootcampViewModel()
    let columns = [GridItem(.flexible()), GridItem(.flexible())]
    
    var body: some View {
        NavigationView {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(viewModel.images, id: \.self) { image in
                        Image(uiImage: image)
                            .resizable()
                            .scaledToFit()
                            .frame(height: 150)
                    }
                }
            }
            .navigationTitle("Async Let 🥳")
        }
    }
}

요런 식으로 뷰랑 뷰모델이 있다고 해봅시다
(저번시간에 했던 거랑 똑같음!)

class TaskGroupBootcampDataManager {
    
    func fetchImagesWithAsyncLet() async throws -> [UIImage] {
        async let fetchImage1 = fetchImage(urlString: "https:/picsum.photos/300")
        async let fetchImage2 = fetchImage(urlString: "https:/picsum.photos/300")
        async let fetchImage3 = fetchImage(urlString: "https:/picsum.photos/300")
        async let fetchImage4 = fetchImage(urlString: "https:/picsum.photos/300")
        
        let (image1, image2, image3, image4) = await (try fetchImage1, try fetchImage2, try fetchImage3, try fetchImage4)
        
        return [image1, image2, image3, image4]
    }
    
    private func fetchImage(urlString: String) async throws -> UIImage {
        guard let url = URL(string: urlString) else {
            throw URLError(.badURL)
        }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url)
            if let image = UIImage(data: data) {
                return image
            } else {
                throw URLError(.badURL)
            }
        } catch {
            throw error
        }
    }
}

fetchImage를 처리해주는 DataManager 클래스를 만들어줌!
그리고 async let으로 비동기 작업이 한번에 모두 같이 시작할 수 있게끔 해주고

class TaskGroupBootcampViewModel: ObservableObject {
    @Published var images: [UIImage] = []
    let manager = TaskGroupBootcampDataManager()
    
    func getImages() async {
        if let images = try? await manager.fetchImagesWithAsyncLet() {
            self.images.append(contentsOf: images)
        }
    }
}

뷰모델에서 manager가 가지고 있는 메소드를 호출해주고!
(원래는 지금처럼 manager를 만들어주기보다는 dependencyInjection으로 넣어주겠죠)

뷰에서 .task로 이미지를 가져오게 해봅시다


뺌!
한번에 다 불러와지죠 이미지


지금은 요렇게 async let으로 하나하나 다 작성해줬는데
한번 TaskGroup을 써서 더 효율적으로 코드를 작성해봅시다



메소드를 하나 만들어주는데
taskGroup이라고 치면 throwing이 붙은게 있고 안붙은게 있다.
에러 처리할건지에 따라서 나뉨


UIImage타입으로 of 파라미터에 넣어주고

이미지를 return 하면서 그 결과 값을 바로 또 return 되게끔 해줌

이 메소드 안에서 addTask를 호출해주는데 priority 설정이 가능하다
기본적으로 task는 parent의 priority를 물려받아서 보통 작성안해줌

func fetchImagesWithTaskGroup() async throws -> [UIImage] {
    return try await withThrowingTaskGroup(of: UIImage.self) { group in
        var images: [UIImage] = []
        
        group.addTask {
            try await self.fetchImage(urlString: "https://picsum.photos/300")
        }
        group.addTask {
            try await self.fetchImage(urlString: "https://picsum.photos/300")
        }
        group.addTask {
            try await self.fetchImage(urlString: "https://picsum.photos/300")
        }
        group.addTask {
            try await self.fetchImage(urlString: "https://picsum.photos/300")
        }
        
        for try await taskResult in group {
            images.append(taskResult)
        }
        
        return images
    }
}

group에다 task를 4개 추가해줬다
그리고 group안에 있는 각각의 task들을 for in 루프를 써서 images array에 append 되게 해줬는데 잘 보면 뭔가 평소에 쓰던 for in 루프랑 다르다는 게 보임
iterate 하면서 돌던 거랑 달리 각각을 동시에 시작하고 결과를 await한다!
그니까 이중에 하나라도 실패하면 계속 기다리게 된다는 거 알아둬야함

func fetchImagesWithTaskGroup() async throws -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
    ]
    
    return try await withThrowingTaskGroup(of: UIImage.self) { group in
        var images: [UIImage] = []
        
        for urlString in urlStrings {
            group.addTask {
                try await self.fetchImage(urlString: urlString)
            }
        }
        
        for try await taskResult in group {
            images.append(taskResult)
        }
        
        return images
    }
}

urlStrings 배열을 만들어주고 for in으로 돌리면서 group에다가 task를 추가해줬다! 더 효율적이고 확장성이 좋아졌죠?


한가지 팁!
images array의 크기를 미리 예약을 해주면 조금 더 메모리를 아낄 수 있음


그리고 아까 for in 루프에서 task 처리될 때 하나라도 실패하면 error 튀어나와서 밖으로 빠져나오게 된다고 했잖음

이걸 방지하려면 UIImage 자체를 옵셔널하게 만들어주고
group.addTask를 try?로 바꿔주면됨
그리고 append하는 부분에서는 옵셔널 바인딩을 해주고!

func fetchImagesWithTaskGroup() async throws -> [UIImage] {
    let urlStrings = [
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
        "https://picsum.photos/300",
    ]
    
    return try await withThrowingTaskGroup(of: UIImage?.self) { group in
        var images: [UIImage] = []
        images.reserveCapacity(urlStrings.count)
        
        for urlString in urlStrings {
            group.addTask {
                try? await self.fetchImage(urlString: urlString)
            }
        }
        
        for try await taskResult in group {
            if let image = taskResult {
                images.append(image)
            }
        }
        
        return images
    }
}

정리하면 이렇게 되겠습니다!
아 그리고 지금처럼 urlStrings는 메소드 내에 작성하기보다는
파라미터로 받는 쪽으로 사용하게 되겠죠!!

profile
우주형

0개의 댓글