본문 바로가기

Swift

async / await 사용해보기

이번 글에서는 Swift에서 비동기 작업을 수행할 때 사용하는 async, await, throws의 개념과 함께, 오류를 세분화하여 처리하기 위한 중첩된 do-catch 구조에 대해 알아보겠습니다.

이 글에서는 fetchNowPlayingMovies라는 실제 예제 함수를 중심으로 설명합니다.

 

func fetchNowPlayingMovies(page: Int = 1) async throws -> TMDBData {
    // Base URL
    guard let url = URL(string: "https://api.themoviedb.org/3/movie/now_playing") else {
        throw APIError.invalidURL
    }
    
    // URL Components
    var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
    components.queryItems = [
        URLQueryItem(name: "language", value: "ko-KR"),
        URLQueryItem(name: "page", value: "\(page)")
    ]
    
    // URLRequest
    guard let finalURL = components.url else {
        throw APIError.invalidURL
    }
    var request = URLRequest(url: finalURL)
    request.httpMethod = "GET"
    request.timeoutInterval = 10
    request.allHTTPHeaderFields = [
        "accept": "application/json",
        "Authorization": "Bearer YOUR_API_KEY"
    ]
    
    // API Call
    do {
        let (data, response) = try await URLSession.shared.data(for: request)
        
        // HTTP 응답 상태 확인
        if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
            throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
        }
        
        // JSON 디코딩
        do {
            var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
            decodedData.type = .noewPlayingMovie
            return decodedData
        } catch {
            throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
        }
    } catch {
        throw APIError.requestFailed("Request Failed: \(error.localizedDescription)")
    }
}

 

 

비동기 함수의 핵심 키워드

  1. async
    • 함수가 비동기 작업을 포함하고 있음을 표시합니다.
    • 이 함수는 호출 시 await 키워드를 사용해야 합니다.
  2. await
    • 비동기 작업의 결과를 기다리기 위해 사용됩니다.
    • 실행 흐름을 일시 중단하지만, 다른 작업은 계속 진행됩니다.
  3. throws
    • 함수 내에서 오류를 던질 수 있음을 나타냅니다.
    • 호출자는 오류를 처리하기 위해 try 또는 do-catch를 사용해야 합니다.

 

사용 방법

fetchNowPlayingMovies 함수를 ViewController에서 호출하는 코드는 아래와 같습니다:

private func fetchMovies() {
    Task {
        do {
            let movies = try await fetchNowPlayingMovies(page: 1)
            DispatchQueue.main.async {
                self.displayMovies(movies)
            }
        } catch {
            DispatchQueue.main.async {
                self.showError(error)
            }
        }
    }
}

사용 시 중요한 점

  1. 비동기 호출
    • Task 또는 다른 async 함수 내에서 await를 사용해 호출해야 합니다.
  2. 에러 처리
    • try와 함께 사용하거나, do-catch 블록을 통해 에러를 명확히 처리해야 합니다.
  3. UI 업데이트
    • 네트워크 작업은 백그라운드에서 실행되므로, UI 변경은 반드시 DispatchQueue.main.async를 사용해야 안전합니다.

 

중첩된 do-catch 구조의 이유

왜 중첩 구조를 사용할까?

함수 fetchNowPlayingMovies에서 do-catch가 중첩된 이유는, 서로 다른 작업 단계에서 발생할 수 있는 오류를 구분하여 처리하기 위해서입니다.

do {
    let (data, response) = try await URLSession.shared.data(for: request)

    // Check HTTP Response Status
    if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
        throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
    }

    // Decode JSON Response
    do {
        var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
        decodedData.type = .noewPlayingMovie
        return decodedData
    } catch {
        throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
    }
} catch {
    throw APIError.requestFailed("Request Failed: \(error.localizedDescription)")
}

 

첫 번째 do-catch: 네트워크 요청 오류

let (data, response) = try await URLSession.shared.data(for: request)

if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
    throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}
  • 역할: 네트워크 요청이 실패하거나 HTTP 응답 상태 코드가 200~299 범위에 들지 않을 경우, 오류를 던집니다.
  • 오류 처리: APIError.requestFailed를 통해 네트워크 관련 오류를 상위로 전달합니다.

두 번째 do-catch: JSON 디코딩 오류

do {
    var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
    decodedData.type = .noewPlayingMovie
    return decodedData
} catch {
    throw APIError.decodingError("Decoding Failed: \(error.localizedDescription)")
}

 

  • 역할: JSON 디코딩 중 데이터 형식이 맞지 않을 경우 오류를 던집니다.
  • 오류 처리: APIError.decodingError를 통해 디코딩 관련 오류를 상위로 전달합니다.

 

왜 하나의 do-catch로 처리하지 않을까?

모든 오류를 단일 do-catch로 처리하면, 어떤 단계에서 오류가 발생했는지 명확히 알기 어렵습니다.

 

단일 do-catch의 문제점:

do {
    let (data, response) = try await URLSession.shared.data(for: request)
    var decodedData = try JSONDecoder().decode(TMDBData.self, from: data)
    decodedData.type = .noewPlayingMovie
    return decodedData
} catch {
    throw APIError.requestFailed("An error occurred: \(error.localizedDescription)")
}

 

 

  • 모든 오류가 동일한 블록에서 처리됩니다.
  • 오류 원인을 구분하기 어렵고, 디버깅이 복잡해질 수 있습니다.

 

throws 위치?

let (data, response) = try await URLSession.shared.data(for: request)

if let httpResponse = response as? HTTPURLResponse, !(200...299).contains(httpResponse.statusCode) {
    throw APIError.requestFailed("HTTP Status Code: \(httpResponse.statusCode)")
}

 

 

  • 이 부분에서 throw는 HTTP 응답 상태 코드가 200~299 범위에 들지 않으면 APIError.requestFailed라는 에러를 발생시키는 역할을 합니다.
  • 왜 if문 안에서 throw를 쓰는지: HTTP 요청이 실패했을 때 그 즉시 에러를 던지기 위해 if문 내부에서 throw를 사용합니다. 만약 이 조건이 충족되면 에러를 던지고, 호출한 곳에서 그 에러를 처리하도록 합니다.
  • 이유: throw가 반드시 catch 블록 내에서만 사용되는 것은 아니며, 함수 내에서 에러를 발생시키고 싶을 때 자유롭게 사용할 수 있습니다.

 

throw가 catch 블록 내에서만 사용되는 것이 일반적인 생각일 수 있지만, 사실 throw는 에러를 발생시키는 기능으로, 어떤 블록에서든 사용할 수 있습니다. 다만, throws가 붙은 함수 내에서만 사용해야 합니다.

 

throw 사용 위치에 대한 설명

throw는 에러를 발생시키기 위한 키워드로, 함수 내의 어떤 곳에서도 사용될 수 있습니다. throw가 쓰인 위치는 에러를 던지려는 의도가 있는 곳입니다.

  1. 에러를 발생시키는 위치:
    • throw코드 실행 중 문제가 발생했을 때 해당 에러를 함수의 호출자에게 전달하려는 목적입니다. 예를 들어, URLSession의 네트워크 요청에서 HTTP 응답 상태 코드가 200~299 범위에 포함되지 않으면 에러를 던지려고 합니다. 이 에러는 호출한 곳에서 처리해야 합니다.
    • if문 내에서 throw를 사용하는 이유 특정 조건이 충족되었을 때(여기선 HTTP 상태 코드가 200~299가 아닐 때) 즉시 에러를 발생시키기 위함입니다. 이 위치에서 에러를 던지면 호출한 곳에서 처리할 수 있습니다.

throws와 throw의 관계

  1. throws는 함수 선언 시 사용:
    • throws는 함수가 에러를 던질 수 있음을 선언하는 역할을 합니다. 즉, throws가 붙은 함수는 throw를 사용하여 에러를 발생시킬 수 있는 함수입니다.
    • 예를 들어, func fetchNowPlayingMovies(page: Int = 1) async throws -> TMDBData라고 선언된 함수에서는 throw를 사용하여 에러를 발생시킬 수 있습니다. 그리고 이 함수는 호출 시 try로 감싸야 하고, 에러를 처리하기 위한 do-catch 블록이 필요합니다.
  2. throw는 에러를 발생시킬 때 사용:
    • throw는 실제로 에러를 던지는 역할을 합니다. 이는 함수 내의 특정 조건에서 발생할 수 있는 문제를 나타내는 데 사용됩니다. 예를 들어, 네트워크 요청에서 HTTP 상태 코드가 유효하지 않을 때 throw를 사용하여 해당 오류를 호출자에게 전달합니다.

throw와 catch의 관계

  • catch는 에러가 발생했을 때 그 에러를 처리하는 역할을 합니다.
  • throw는 에러를 발생시키기 위한 키워드이므로, throw를 사용하여 에러를 발생시키면 그 에러는 해당 함수에서 호출한 곳으로 전달됩니다. 이 에러를 처리하기 위해 호출자는 do-catch 블록을 사용하여 그 에러를 잡고 처리해야 합니다.

다시 말해, throw는 반드시 throws 함수 안에서만 사용해야 한다는 규칙이 있습니다. 함수 선언부에 throws가 있으면 그 함수 내에서는 자유롭게 throw를 사용하여 에러를 발생시킬 수 있습니다.

 

'Swift' 카테고리의 다른 글

setCustomSpacing(_:after:)의 역할  (0) 2025.01.05
UICollectionViewCompositionalLayout 관련 데이터 소스 관리  (0) 2025.01.03
async/await란?  (0) 2024.12.18
async과 await 개요  (0) 2024.12.10
컨텍스트 (context)는 무엇인가요?  (1) 2024.12.10