[Swift] Moya + async-await

λ―Όκ²½μ€€Β·2025λ…„ 2μ›” 24일
1

Moya와 Swift Concurrency

iOS κ°œλ°œμ—μ„œ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ μ²˜λ¦¬ν•  λ•Œ, URLSession을 직접 μ‚¬μš©ν•˜λŠ” 것은 번거둜운 μž‘μ—…μ΄ 될 수 μžˆμŠ΅λ‹ˆλ‹€. 이λ₯Ό 더 κ°„κ²°ν•˜κ³  효율적으둜 λ§Œλ“€κΈ° μœ„ν•΄ λ§Žμ€ κ°œλ°œμžλ“€μ΄ Moyaλ₯Ό ν™œμš©ν•˜κ³  μžˆμŠ΅λ‹ˆλ‹€. MoyaλŠ” Alamofire 기반의 λ„€νŠΈμ›Œν¬ 라이브러리둜, API μš”μ²­μ„ 보닀 ꡬ쑰적으둜 관리할 수 μžˆλ„λ‘ λ„μ™€μ€λ‹ˆλ‹€.

ν•˜μ§€λ§Œ κΈ°μ‘΄ MoyaλŠ” completion handler 기반의 비동기 처리λ₯Ό μ‚¬μš©ν•˜κΈ° λ•Œλ¬Έμ—, Swift의 μ΅œμ‹  Concurrency(비동기/λ™μ‹œμ„±) κΈ°λŠ₯κ³Ό μžμ—°μŠ€λŸ½κ²Œ μ–΄μšΈλ¦¬μ§€ μ•ŠλŠ” λ¬Έμ œκ°€ μžˆμŠ΅λ‹ˆλ‹€. λ‹€ν–‰νžˆλ„, Swift의 async/await을 ν™œμš©ν•˜λ©΄ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ 훨씬 더 μ§κ΄€μ μœΌλ‘œ μž‘μ„±ν•  수 있으며, 가독성과 μœ μ§€λ³΄μˆ˜μ„±μ„ 크게 ν–₯μƒμ‹œν‚¬ 수 μžˆμŠ΅λ‹ˆλ‹€.

이 κΈ€μ—μ„œλŠ” Moya와 Swift Concurrencyλ₯Ό κ²°ν•©ν•˜μ—¬ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ λ”μš± κΉ”λ”ν•˜κ³  효율적으둜 μ²˜λ¦¬ν•˜λŠ” 방법을 μ†Œκ°œν•©λ‹ˆλ‹€. withCheckedContinuationκ³Ό withCheckedThrowingContinuation을 ν™œμš©ν•˜μ—¬ κΈ°μ‘΄ Moya의 completion 기반 APIλ₯Ό async/await λ°©μ‹μœΌλ‘œ λ³€ν™˜ν•˜λŠ” 방법을 μ•Œμ•„λ³΄κ³ , μ—λŸ¬ 핸듀링을 보닀 μœ μ—°ν•˜κ²Œ μ²˜λ¦¬ν•  수 μžˆλŠ” μ „λž΅λ„ ν•¨κ»˜ λ‹€λ€„λ³΄κ² μŠ΅λ‹ˆλ‹€. πŸš€

🟑 1. Concurrency ν•¨μˆ˜.

1. withCheckedContinuation

βœ… 기본적으둜 Result νƒ€μž…μ„ μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 비동기 ν•¨μˆ˜ λ³€ν™˜μ— μ‚¬μš©

  • 비동기 ν•¨μˆ˜κ°€ 였λ₯˜λ₯Ό λ˜μ§€μ§€ μ•ŠλŠ” 경우(Result νƒ€μž…μ΄λ‚˜ Errorλ₯Ό μ‚¬μš©ν•˜μ§€ μ•ŠλŠ” 경우)
  • continuation.resume(returning:)을 μ‚¬μš©ν•΄ 값을 λ°˜ν™˜

μ‚¬μš©μ˜ˆμ‹œ

func fetchData(completion: @escaping (String) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        completion("Hello, Swift Concurrency!")
    }
}

func fetchDataAsync() async -> String {
    return await withCheckedContinuation { continuation in
        fetchData { result in
            continuation.resume(returning: result)
        }
    }
}

Task {
    let result = await fetchDataAsync()
    print(result)  // "Hello, Swift Concurrency!"
}

πŸ“Œ μ„€λͺ…

  • fetchData(completion:)은 기쑴의 콜백 기반 ν•¨μˆ˜
  • fetchDataAsync()μ—μ„œ withCheckedContinuation을 μ‚¬μš©ν•΄ async ν•¨μˆ˜λ‘œ λ³€ν™˜
  • continuation.resume(returning:)을 ν˜ΈμΆœν•΄ 데이터λ₯Ό async ν•¨μˆ˜μ˜ 결과둜 λ°˜ν™˜

2. withCheckedThrowingContinuation

βœ… 였λ₯˜λ₯Ό λ˜μ§€λŠ” 비동기 ν•¨μˆ˜ λ³€ν™˜μ— μ‚¬μš©

  • completionHandlerκ°€ Result<T, Error> λ˜λŠ” (T?, Error?) ν˜•νƒœλ‘œ 값을 λ°˜ν™˜ν•˜λŠ” 경우 적합
  • continuation.resume(throwing:)을 μ‚¬μš©ν•΄ 였λ₯˜ 전달 κ°€λŠ₯

μ‚¬μš©μ˜ˆμ‹œ

enum NetworkError: Error {
    case invalidResponse
}

func fetchDataWithError(completion: @escaping (String?, Error?) -> Void) {
    DispatchQueue.global().asyncAfter(deadline: .now() + 1) {
        let success = Bool.random()
        if success {
            completion("Success Data", nil)
        } else {
            completion(nil, NetworkError.invalidResponse)
        }
    }
}

func fetchDataAsyncWithError() async throws -> String {
    return try await withCheckedThrowingContinuation { continuation in
        fetchDataWithError { result, error in
            if let error = error {
                continuation.resume(throwing: error)  // 였λ₯˜ 전달
            } else if let result = result {
                continuation.resume(returning: result)  // 정상 데이터 λ°˜ν™˜
            } else {
                continuation.resume(throwing: NetworkError.invalidResponse)  // μ˜ˆμ™Έ 처리
            }
        }
    }
}

Task {
    do {
        let result = try await fetchDataAsyncWithError()
        print(result)  
    } catch {
        print("Error:", error)
    }
}

πŸ“Œ μ„€λͺ…

  • fetchDataWithError(completion:)은 성곡/μ‹€νŒ¨ 여뢀에 따라 데이터λ₯Ό λ°˜ν™˜ν•˜λŠ” 기쑴의 콜백 기반 ν•¨μˆ˜
  • fetchDataAsyncWithError()μ—μ„œλŠ” withCheckedThrowingContinuation을 μ‚¬μš©ν•΄ async/await λ²„μ „μœΌλ‘œ λ³€ν™˜
  • continuation.resume(throwing:)을 μ‚¬μš©ν•΄ 였λ₯˜λ₯Ό 던짐

3. withCheckedContinuation vs withCheckedThrowingContinuation 차이점

withCheckedContinuationwithCheckedThrowingContinuation
였λ₯˜ 처리Errorλ₯Ό 던질 수 μ—†μŒErrorλ₯Ό 던질 수 있음
resume 방식continuation.resume(returning:)continuation.resume(returning:) λ˜λŠ” continuation.resume(throwing:)
μ‚¬μš© μ˜ˆμ‹œλ‹¨μˆœ 비동기 콜백 λ³€ν™˜Result<T, Error> λ˜λŠ” (T?, Error?) ν˜•νƒœμ˜ 비동기 처리

4. μ‚¬μš© μ‹œ μ£Όμ˜ν•  점 🚨

  1. resume(_:)은 λ°˜λ“œμ‹œ ν•œ 번만 ν˜ΈμΆœν•΄μ•Ό 함

    • resume(returning:) λ˜λŠ” resume(throwing:)을 두 번 이상 ν˜ΈμΆœν•˜λ©΄ λŸ°νƒ€μž„ 였λ₯˜ λ°œμƒ
    • μ˜ˆμ œμ—μ„œ if let / else if let을 μ‚¬μš©ν•˜μ—¬ 쀑볡 호좜 λ°©μ§€
  2. Completion Handler 기반 APIκ°€ μ—¬λŸ¬ 번 ν˜ΈμΆœλ˜μ§€ μ•ŠλŠ”μ§€ 확인

    • 일뢀 APIλŠ” μ—¬λŸ¬ 번 completion을 ν˜ΈμΆœν•  수 있음 β†’ 직접 λ°©μ–΄ μ½”λ“œ μΆ”κ°€ ν•„μš”
    • 예λ₯Ό λ“€μ–΄ URLSession의 dataTask(with:)λŠ” λ„€νŠΈμ›Œν¬ μš”μ²­μ΄ μ·¨μ†Œλ˜μ—ˆμ„ λ•Œλ„ completion을 ν˜ΈμΆœν•  수 있음

🟒 2. Moya + aysnc-await

1. Moya 라이브러리의 핡심 λ©”μ„œλ“œ.

Moya 라이브러리의 request ν•¨μˆ˜λŠ” λ„€νŠΈμ›Œν¬ μš”μ²­μ„ μˆ˜ν–‰ν•˜λŠ” 핡심 λ©”μ„œλ“œμž…λ‹ˆλ‹€.
이 ν•¨μˆ˜λŠ” TargetType을 기반으둜 API μš”μ²­μ„ λ§Œλ“€κ³ , κ²°κ³Όλ₯Ό completion ν΄λ‘œμ €λ₯Ό 톡해 λ°˜ν™˜ν•©λ‹ˆλ‹€.

ν•¨μˆ˜ μ›ν˜•

open func request(
    _ target: Target, 
    callbackQueue: DispatchQueue? = .none, 
    progress: ProgressBlock? = .none, 
    completion: @escaping Completion
) -> Cancellable

λ§€κ°œλ³€μˆ˜

λ§€κ°œλ³€μˆ˜νƒ€μž…μ„€λͺ…
targetTargetμš”μ²­ν•  API의 λŒ€μƒ(TargetType을 λ”°λ₯΄λŠ” νƒ€μž…)
callbackQueueDispatchQueue?응닡을 μ²˜λ¦¬ν•  큐 (κΈ°λ³Έκ°’ nil β†’ λ‚΄λΆ€μ μœΌλ‘œ μ μ ˆν•œ 큐 μ‚¬μš©)
progressProgressBlock?λ‹€μš΄λ‘œλ“œ/μ—…λ‘œλ“œ μ§„ν–‰ μƒνƒœλ₯Ό 좔적할 ν΄λ‘œμ € (κΈ°λ³Έκ°’ nil)
completion@escaping Completionμš”μ²­ μ™„λ£Œ ν›„ ν˜ΈμΆœλ˜λŠ” ν΄λ‘œμ € (Result<Response, MoyaError> ν˜•νƒœμ˜ κ²°κ³Ό λ°˜ν™˜)

λ°˜ν™˜κ°’

  • Cancellable : μš”μ²­μ„ μ·¨μ†Œν•  수 μžˆλŠ” 객체λ₯Ό λ°˜ν™˜
    (cancel()을 ν˜ΈμΆœν•˜λ©΄ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ 쀑단할 수 있음)

2. MoyaProvider Extension

import Moya

extension MoyaProvider {
	func request(
        _ target: Target, 
        callbackQueue: DispatchQueue? = .none, 
        progress: ProgressBlock? = .none, 
        completion: @escaping Completion
    ) async -> Result<Moya.Response, MoyaError> {
    	await withCheckContinuation { continuation in 
        	self.request(
            	target, 
                callbackQueue: callbackQueue, 
                progress: progress,
            ) { result in 
            	continuation.resume(returning: result)
            }
        }
    }
}

πŸ“Œ μ„€λͺ…

  • withCheckedContinuation을 μ‚¬μš©ν•˜μ—¬ λ„€νŠΈμ›Œν¬ μš”μ²­μ„ async λ°©μ‹μœΌλ‘œ λ³€ν™˜
  • κΈ°μ‘΄ Moya의 Completion νƒ€μž…(Result<Response, MoyaError>)을 κ·ΈλŒ€λ‘œ λ°˜ν™˜
  • throw 없이 Result νƒ€μž…μœΌλ‘œ κ²°κ³Όλ₯Ό μ „λ‹¬ν•˜λ―€λ‘œ, λ„€νŠΈμ›Œν¬ κ³„μΈ΅μ—μ„œ ν•œ 번 더 μ—λŸ¬ 핸듀링 κ°€λŠ₯

3. λ„€νŠΈμ›Œν¬ λͺ¨λ“ˆμ—μ„œ μ—λŸ¬ 핸듀링

λ„€νŠΈμ›Œν¬ μš”μ²­μ„ μˆ˜ν–‰ν•œ ν›„, Resultλ₯Ό ν™œμš©ν•˜μ—¬ μ—λŸ¬λ₯Ό λͺ¨λ“ˆ λ‚΄λΆ€μ—μ„œ ν•œ 번 더 κ°€κ³΅ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

class Network {
    /// λ„€νŠΈμ›Œν¬ μš”μ²­μ„ μˆ˜ν–‰ν•˜κ³ , λ‚΄λΆ€μ—μ„œ μ—λŸ¬ ν•Έλ“€λ§ν•œ ν›„ λ°˜ν™˜
    func task<T: Decodable, E: TargetType>(_ target: E) async throws -> T {
        let provider: MoyaProvider<E> = .init()
        let result = await provider.request(target)
        let parsed = self.parsing(T.self, result: result)
        
        switch parsed {
        case .success(let response):
            return .success(response)
        case .failure(let moyaError):
            let networkError = self.moyaError(moyaError)
            throw networkError
        }
    }
    
    /// MoyaError β†’ NetworkError둜 λ³€ν™˜ν•˜λŠ” λ©”μ„œλ“œ
    private func moyaError(_ error: MoyaError) -> NetworkError {
        switch error {
        case .statusCode(let response):
            return .serverError(response.statusCode)
        case .underlying(let nsError, _):
            return .networkFailure(nsError.localizedDescription)
        case .requestMapping, .parameterEncoding, .jsonMapping:
            return .invalidRequest
        @unknown default:
            return .unknown
        }
    }
    
    /// JSON Dataλ₯Ό νŒŒμ‹±ν•˜λŠ” λ©”μ„œλ“œ
    private func parsing<T: Decodable>(_ type: T.Type, result: <Moya.Response, MoyaError>) -> Reseult<T, MoyaError> {
    	switch result {
        case .success(let response):
        	if let data = try? JSONDecoder().decode(type, from: response.data) {
            	return .success(data)
            } else {
            	return .failuer(.jsonMapping(response))
            }
        case .failure(let moyaError):
        	return .failure(moyaError)
        }
    }
}

/// μ»€μŠ€ν…€ λ„€νŠΈμ›Œν¬ μ—λŸ¬ μ •μ˜
enum NetworkError: Error {
    case serverError(Int)  // μ„œλ²„ μƒνƒœ μ½”λ“œ 였λ₯˜
    case networkFailure(String)  // λ„€νŠΈμ›Œν¬ μ‹€νŒ¨ (인터넷 μ—°κ²° 문제 λ“±)
    case invalidRequest  // 잘λͺ»λœ μš”μ²­
    case unknown  // μ•Œ 수 μ—†λŠ” 였λ₯˜
}

πŸ“Œ μ„€λͺ…

  • Networkλ₯Ό 톡해 MoyaProviderλ₯Ό κ°μ‹Έμ„œ μ—λŸ¬λ₯Ό ν•œ 번 더 μ²˜λ¦¬ν•˜λŠ” 계측을 λ§Œλ“¦
  • task(_:)μ—μ„œ request(_:) 호좜 ν›„ Result<Response, MoyaError>λ₯Ό λ””μ½”λ”© νƒ€μž…μœΌλ‘œ λ³€ν™˜ν•˜μ—¬ 리턴 λ˜λŠ” MoyaErrorλ₯Ό NetworkError둜 λ§€ν•‘ν•˜μ—¬ throw
  • parsing(_:result:)λ₯Ό 톡해 Responseλ₯Ό T.Type으둜 λ””μ½”λ”©
  • moyaError(_:)을 톡해 MoyaErrorλ₯Ό NetworkError둜 λ§€ν•‘

4. μ‚¬μš© 예제

let network = Network()

Task {
	do {
    	let target: API = .getUserName(id: 124)
    	let value: String = try await network.task(target)
    	
        print("βœ… μœ μ € 이름: \(value)")
    } catch {
        print("❌ λ„€νŠΈμ›Œν¬ 였λ₯˜: \(error)")    
    }
}

πŸ“Œ μ„€λͺ…

  • network.task(target)을 ν˜ΈμΆœν•˜λ©΄ T.Type을 λ°˜ν™˜
  • do-try-catch문을 μ‚¬μš©ν•΄ λ„€νŠΈμ›Œν¬ 응닡을 처리
  • NetworkError둜 λ³€ν™˜λœ μ—λŸ¬λ₯Ό catchν•˜μ—¬ 더 λͺ…ν™•ν•œ 였λ₯˜ λ©”μ‹œμ§€λ₯Ό 제곡

βœ… 3. 정리

  • 이 방식은 λ„€νŠΈμ›Œν¬ λͺ¨λ“ˆμ—μ„œ μ—λŸ¬λ₯Ό ν•œ 번 더 가곡할 수 μžˆμ–΄ ν™•μž₯성이 λ›°μ–΄λ‚©λ‹ˆλ‹€.
  • async/await을 μ‚¬μš©ν•˜λ©΄μ„œλ„ 보닀 μ²΄κ³„μ μœΌλ‘œ μ—λŸ¬ 처리λ₯Ό ν•˜κ³  μ‹Άλ‹€λ©΄ μ μ ˆν•œ 선택이 될 κ²ƒμž…λ‹ˆλ‹€! πŸš€
profile
iOS Developer πŸ’»

0개의 λŒ“κΈ€