📁 Main 스레드에서 호출하도록 보장하기
⛓️💥 초기 코드
func fetchMovieData() {
Task {
do {
let now = try await networkingService.fetchMovies(type: .NowPlaying)
let up = try await networkingService.fetchMovies(type: .upcoming)
let pop = try await networkingService.fetchMovies(type: .popular)
self.nowPlaying = now
self.upcoming = up
self.popular = pop
self.onUpdate?()
} catch {
print(error)
}
}
}
viewModel에서 URLSession을 사용해서 UI를 그리는데 필요한 데이터를 받아오는 함수이다.
networkingService.fetchMovies는 비동기 함수
self.onUpdate?(): 이 함수는 UI를 업데이트하는 함수라서 main thread를 보장해야 한다.
⚠️ 주의점
네트워크 요청(fetchMovies)은 비동기라서, 완료 시점이 메인 스레드가 아닐 수 가 있었다.
onUpdate에서 VC가 reloadData()를 하고, onError에서 Alret을 띄울기 예정이기 때문에 무조건 메인에서 해야 안전한 상황이었다.
- 단순
Task으로는 메인 보장을 확신할 수 없다.
await를 3번 순차적으로 기다리기 때문에 상대적으로 느렸다.
♻️리팩토링 1️⃣: MainActor.run 사용
func fetchMovieData() {
Task {
do {
async let NowPlaying = networkingService.fetchMovies(type: .nowPlaying)
async let upcoming = networkingService.fetchMovies(type: .upcoming)
async let popular = networkingService.fetchMovies(type: .popular)
let (now, up, pop) = try await (NowPlaying, upcoming, popular)
self.nowPlaying = now
self.upcoming = up
self.popular = pop
await MainActor.run { self.onUpdate?() }
} catch {
await MainActor.run { self.onErrorMessage?("...") }
}
}
}
💡 개선점
onUpdate / onErrorMessage?("...")가 항상 메인에서 실행됨(VC에서 안전하게 실행)
async let로 요청들을 동시에 시작해서 가장 오래 걸리는 요청 기준으로 끝나도록 속도개선
⚠️ 주의점
- 호출하는 쪽에서는 그냥 단순 함수처럼 호출되기 때문에 흐름제어가 어려움
- 코드의 의도가 불분명함(
UI와 관련된 부분은 self.onUpdate?()와 self.onErrorMessage?("...")뿐이라고 읽힐수 있음 )
♻️리팩토링 2️⃣: @MainActor 사용
func fetchMovieData() async {
do {
async let nowPlaying = networkingService.fetchMovies(type: .NowPlaying)
async let upcoming = networkingService.fetchMovies(type: .upcoming)
async let popular = networkingService.fetchMovies(type: .popular)
let (now, up, pop) = try await (nowPlaying, upcoming, popular)
self.nowPlaying = now
self.upcoming = up
self.popular = pop
self.onUpdate?()
} catch {
self.onErrorMessage?(makeErrorMessage(error))
}
}
💡 개선점
- 메인 액터가 함수 전체를 보장해서
popular 같은 파라미터의 상태 업데이트와 self.onUpdate?()같은 콜백 호출이 한 군데(MainActor)에서 처리되어서 안전함.
- 어차피
let (now, up, pop) = try await (nowPlaying, upcoming, popular)에 해당하는 네트워크 통신은 저절로 백그라운드 실행되고 결과 받는 시점에 다시 MainActor로 복귀하기 때문에 상관없음
VC는 reloadData()와Alret처리 같은 UI만 맡을 수 있음
VC는 Task { await viewModel.fetchMovieData() }로 호출만 담당하도록 분리
⚠️ 주의점
@MainActor에서 무거운 CPU 작업(대량 정렬이나 복잡한 가공 등)을 오래 하면 UI 버벅일 수 있기 때문에 지금처럼 단순 배열 대입이나 콜백 정도일 경우만 이런식으로 진행하는 것이 좋겠다.