Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
iOS Developer/Concurrency

4. async let

by Toughie 2023. 7. 12.

async let 

VStack {
...
}
.onAppear {
    Task {
        await doSomething()
        await doSomething2()
        await doSomething3()
        ...
    }
}

위 간단한 예시를 보면, Task 내부에서 비동기 작업들이 순차적으로 실행될 것이다.

doSomething의 완료를 기다리고 -> 2가 실행되고 완료되고 나서 -> 3이 실행될 것이다.

이렇게 순차적으로 하나하나 기다리는 대신, 비동기적으로 각각의 작업이 수행되도록 할 수는 없을까?

여기서 async let을 사용할 수 있다.


먼저 URL을 통해 여러 이미지를 다운 받는 상황을 가정해보자.

네비게이션 스택 -> 스크롤뷰 -> LazyVGrid 내부에 이미지를 표시한다.

import SwiftUI

struct AsyncLetPrac: View {
    
    @State private var images: [UIImage] = []
    @State private var navTitle: String = "Async Let"
    
    let columns = [GridItem(.flexible()), GridItem(.flexible())]
    
    
    var body: some View {
        NavigationStack {
            ScrollView {
                LazyVGrid(columns: columns) {
                    ForEach(images, id: \.self) { image in
                        Image(uiImage: image)
                            .resizable()
                            .scaledToFit()
                            .frame(height: 200)
                    }
                }
            }
            .navigationTitle(navTitle)
            .onAppear {
            
            ...
            
            }
...

 

이미지를 다운로드하는 async 메소드는 아래와 같다.

    func fetchImage() async throws -> UIImage {
        guard let url = URL(string: "https://picsum.photos/200") else { return UIImage() }
        
        do {
            let (data, _) = try await URLSession.shared.data(from: url, delegate: nil)
            if let image = UIImage(data: data) {
                return image
            } else {
                throw URLError(.badURL)
            }
        } catch {
            throw error
        }
    }

이제 onAppear에서 Task를 통해 위 메서드를 실행시켜 배열에 추가해보자.

ScrollView {
    ...
}
.onAppear {
    Task {
        do {
            let image1 = try await fetchImage()
            self.images.append(image1)
            
            let image2 = try await fetchImage()
            self.images.append(image2)
            
            let image3 = try await fetchImage()
            self.images.append(image3)
            
            let image4 = try await fetchImage()
            self.images.append(image4)
            }
        ...

이러면 초반 예시와 같이 각각의 이미지 리턴을 순차적으로 기다리기 때문에 

뷰에 이미지 1, 2, 3, 4가 순차적으로 뜨게 될 것이다.

물론.. 이걸 의도하는 경우라면 그럴 수도 있지만 한 번에 촥! 뜨는게 더 자연스러운 편이라 생각한다.

 

그럼 Task를 2개로 나눠서 실행한다면?

이미지 1, 2가 착착 뜨고 그 다음 이미지 3, 4가 착착 뜰 것이다.

흠... 그냥 태스크 하나만 쓰고 이미지 한 번에 다 기다렸다가 화면에 한 번에 쫙 뿌려주고 싶은데?

Task {
    do {
        async let fetchImage1 = fetchImage()
        async let fetchImage2 = fetchImage()
        async let fetchImage3 = fetchImage()
        async let fetchImage4 = fetchImage()

위와 같이 각각의 작업을 비동기로 실행하고, 결과값을 저장한다.

// 1. 모든 비동기 작업이 완료될 때까지 기다린 후에 결과를 할당하는 방식
let (image1, image2, image3, image4) = try await (fetchImage1, fetchImage2, fetchImage3, fetchImage4)

// 2. 비동기 작업을 동시에 실행하고, 완료될 떄마다 결과를 할당하는 방식
//다만 중간에 하나라도 에러를 던지면 catch블록으로 넘어갈 것이다.
let (image1, image2, image3, image4) = await (try fetchImage1, try fetchImage2, try fetchImage3, try fetchImage4)

// 3. 에러를 던지는 대신 nil을 return하도록(catch 블럭 진입을 막기 위해) 대신 바인딩이 필요함.
 let (image1, image2, image3, image4) = await (try? fetchImage1, try? fetchImage2, try? fetchImage3, try? fetchImage4)
 
 
 여기서는 2번 방식을 사용하겠다.
do {
    async let fetchImage1 = fetchImage()
    async let fetchImage2 = fetchImage()
    async let fetchImage3 = fetchImage()
    async let fetchImage4 = fetchImage()

    let (image1, image2, image3, image4) = await (try fetchImage1, try fetchImage2, try fetchImage3, try fetchImage4)
    self.images.append(contentsOf: [image1, image2, image3, image4])
} catch {
    throw error
}

위와 같이 async let을 사용하면 비동기 작업을 동시에 실행하고, 각각의 작업이 다 완료될 때까지 기다림.

cf. 각각 await 하는 것이 아니라, await이 한 번만 쓰인 것을 참고!

이미지 로딩 다 될 때까지 기다렸다가 한번에 배열에 추가!

이러면 뷰에 여러 이미지가 동시에 짠 하고 나타난다.


async let을 활용할 때, 반환 타입이 다른 메서드도 함께 사용할 수 있다.

위에서 사용한 메서드는 UIImage를 리턴하는 메서드였기에,

아래와 같이 String 값을 리턴하는 메서드를 만들어 테스트해보자.

    func fetchTitle() async -> String {
        return "New Title"
    }

*async 내부에서 반드시 비동기 작업을 수행해야 하는 것은 아님.

async는 비동기적으로 실행되는 코드 블록을 정의할 수 있는 기능을 제공하는 것.

딱 보고 아 비동기로 실행되는 메서드인가보다~를 표현하기 위해서도 있음.

 

보통은 비동기 작업을 내부에서 수행하지만, 수행하지 않는 경우에도 async 메서드를 사용할 수 있는 이유는 아래와 같다.

async 메서드 내에서 동기적인 작업을 수행하더라도, 해당 메서드가 async로 선언되어 있다면

나중에 필요한 경우 내부 작업을 비동기로 변경하거나 추가할 수 있기 때문에 유연성이 생긴다.

do {
    async let fetchImage1 = fetchImage()
    async let fetchTitle = fetchTitle()
    
    let (image, title) = await (try fetchImage1, try fetchTitle)
    self.images.append(image)
    self.navTitle = title
} catch {
    throw error
}

이렇게 Task 내부에서 반환타입이 다른, 각각의 async 메서드를 async let을 활용해 동시에 호출해 작업을 할 수 있다.


이렇게 async let을 활용해서 여러 비동기 작업을 한 태스크 내부에서 호출하는 방식을 알아봤다.

각각의 비동기 작업을 await 하는 것이 아니라, 각 비동기 작업을 동시에 실행한 후에 한 번에 모두의 완료 결과를 기다렸다 받는 방식!

이번 예시의 경우 태스크를 취소하면 내부 비동기 작업들이 전부 취소될 것이고,

태스크의 우선순위를 변경하면 우선순위도 같이 변경될 것이다.

 

다만 비동기 작업의 개수가 그리 많지 않다면 이번 예시와 같은 방식을 사용해도 괜찮겠지만,

만약 수십개 혹은 그 이상의 비동기 작업을 동시에 실행하는 경우라면?

async let 하드코딩을 하는 것은 대단히 비효율적일 것이다.

 

이를 위해서 존재하는 개념이 바로 Task Group이다.

Task Group은 다음 포스팅에서 다뤄보도록 하겠다!