티스토리 뷰

반응형

 

안녕하세요 :) Zedd입니다.

오늘은!!!! 주말을 맞아 그동안 미루고 미뤄왔던 Combine -> async/await로 바꾸기...ㅋㅋ..

아주 간단한 앱이어서 호다닥 바꿔볼 수 있을 것 같습니다.

참고 : 이 앱은 Deployment Target이 15.0입니다..

 

# 구조

API호출이 딱 하나 있는 아주 간단한 SwiftUI앱입니다. 

[API.swift]

위와 같이 Combine을 사용해서 network request를 수행하고

 

[Service.swift]

service쪽에서 API에 있는 perfom 메소드를 수행합니다. 

 

[ViewModel.swift]

그래서 ViewModel쪽에서 

이렇게 service의 request를 호출하여 응답을 @Published로 mark된 변수에 넣어줍니다.

대충 진짜 뻔한 SwiftUI 앱인거 아시겠죠..!?!?

 

# Swift Concurrency 

됐고...async 부터 달아본다..

[API.swift]

AS-IS
func perform<T: Decodable>(method: HTTPMethod, url: String?) -> AnyPublisher<HTTPResponse<T>, Error> {

TO-BE ✅
func perform<T: Decodable>(method: HTTPMethod, url: String?) async throws -> HTTPResponse<T>

1.  AnyPublisher 삭제

2. 메소드에 async throws 추가 

리턴타입이 바뀌었으니 return부분도 수정되어야되겠죠? 

AS-IS
return URLSession.shared.dataTaskPublisher(for: request)
    .tryMap { result -> HTTPResponse<T> in
        let value = try self.decoder.decode(T.self, from: result.data)
        return HTTPResponse(value: value, response: result.response)
    }
    .receive(on: DispatchQueue.main)
    .eraseToAnyPublisher()
    
    
TO-BE ✅
let (data, urlResponse) = try await URLSession.shared.data(from: url)
let response = try decoder.decode(T.self, from: data)
return HTTPResponse(value: response, response: urlResponse)

dataTaskPublisher대신 data(from:) 이라는 async 메소드를 사용하도록 바꿨습니다.

당연히 await 과 함께 불려줘야겠죠!! 

그냥 동기 메소드처럼 리턴해주면 끝...

⚠️ URLSession.shared.data는 iOS 15부터 사용가능 ㅋㅋ... **

 

[Service.swift]

service쪽에서 .perform(~~) 을 수행하던 곳이 있겠죠?

위 메소드도 수정이 필요할 것 같습니다.

역시나 일단 메소드 정의부에 async를 붙히고 

AS-IS
func request() -> AnyPublisher<[SomeType], Error> {}

TO-BE ✅
func request() async throws -> [SomeType]

return역시 수정해줍시다.

AS-IS
return API.shared.perform(method: .get, url: component?.string)
	.map(\.value)
	.eraseToAnyPublisher()
    
TO-BE ✅
return try await API.shared.perform(method: .get, url: component?.string).value

끝!

 

[ViewModel.swift]

이제 viewModel쪽에서 service.request().sink~~ 하던 부분이 있을텐데요.

요기도 바뀌어야겠죠

 Zedd : 응 일단 async 붙혀~~~

🥲 : 하...그럼 View에서 viewModel.requestSomething하던 곳들 어떡하지...에러나는데..

SomeView()
    ...
    .onAppear(perform: { // 🚨 Invalid conversion from 'async' function of type '() async -> Void' to synchronous function type '() -> Void'
        await self.viewModel.requestSomething()
    })

(위 코드는 예제코드입니다.. iOS 15에서 task modifier가 나왔으니 쓸 수 있다면 task를 쓰시는게...)

 

이때 2가지 방법이 있습니다.

1. ViewModel > 메소드 > Task안에서 async메소드 호출하기

2. ViewModel > async 메소드로 바꾸고 호출하는 쪽이 Task안에서 호출하기

 

# Task? (feat. ViewModel > 메소드 > Task안에서 async메소드 호출하기)

Task가 뭔지 모르시는분들도 계실텐데요!

아주아주 간단하게 말해서..non-concurrent한 메소드 안에 concurrent한 환경을 만들어주는 역할을 하게 됩니다.

ViewModel.swift에 있는 requestSomething은 다음과 같이 non-concurrent 메소드인데요.

원래 async메소드를 호출하려면 requestSomething역시 concurrent 메소드가 되어야 합니다.

하지만 아래와같이

@Published var response: [SomeType] = []

func requestSomething()  {
    Task {
        self.response = try await self.service.request()
    }
}

 Task가 non-concurrent한 메소드 안에 concurrent한 환경을 만들어주기 때문에 이렇게 호출할 수 있는 것이죠.

그리고 호출부에서도

SomeView()
    ...
    .onAppear(perform: { 
        self.viewModel.requestSomething()
    })

async메소드가 아니기 때문에 await 없이 그냥 호출해주면 됩니다.

 

1. ViewModel > 메소드 > Task안에서 async메소드 호출하기 ✅

2. ViewModel > async 메소드로 바꾸고 호출하는 쪽이 Task안에서 호출하기

1번은 방금 봤으니 패스... 2번은 어떤건지 감이 오시나요?

 

# ViewModel > async 메소드로 바꾸고 호출하는 쪽이 Task안에서 호출하기

@Published var response: [SomeType] = []

func requestSomething() async  {
    do {
        self.response = try await self.service.request()
    } catch {
        // some code
    }
}

ViewModel.swift 쪽 메소드를 async 메소드로 바꾸고,

requestSomething을 호출하는 호출부를 Task로 감싸주면 됩니다.

SomeView()
    ...
    .onAppear(perform: {
     	Task {
            await self.viewModel.requestSomething()
         }
    })

이렇게! 

그러면 onAppear같이 non-concurrent한 메소드 안에서 concurrent 환경이 만들어지는것이죠.

 

⚠️ ---------- ⚠️

onAppear대신 iOS 15에서 나온 task modifier를 쓰면

.task {
    await self.viewModel.requestSomething()
}

그냥 바로 이렇게 가능! 

참고 : SwiftUI, iOS 15+ ) onAppear()대신 task()


⚠️ ---------- ⚠️

 

# 느낀점 

1. Task만 있다면 Combine -> Swift Concurrency는 어렵지 않겠다..? 라는 생각이 들었습니다.

이게 Combine이 아니라 RxSwift여도요!

(근데 RxSwift도 Swift Concurrency지원하게 업데이트 됨)

 

2. URLSession.shard.data가 iOS 15부터 사용가능한건 좀 ㅠ...

여기가 async로 깔끔하게 안풀리면 끔찍한 혼종이 발생할 것 같다는 생각이 들었습니다. 

아직 해보진 않음...

 

앱이 워낙 간단하다보니 딱히 트러블슈팅도 없었고....그래서 딱히 느낀점으로 쓸만한게 없네요. 


 

이렇게 아주 간단하게 Combine ->  Swift Concurrency를 사용하도록 바꿔봤는데

저도 잘 몰라서 틀린 부분이 있을지도 모르겠습니다 ㅎ (특히 Task...)

Task는 공부를 제대로 한번 해봐야겠어요. iOS 13부터 사용가능하니..많이 쓰게 될 듯한ㅎㅎ

틀린 부분이나 개선될 수 있는 부분들을 발견하셨다면 댓글 남겨주세요! 

 

반응형