[iOS/Swift] @escaping - 네트워킹 코드에서 함수의 반환값을 사용하지 않는 이유

네트워크 통신 기능을 구현하는 경우 서버에서 온 응답값을 `completionHandler`를 통해 `@escaping`의 형태로 함수 밖으로 값을 빼내는 방식을 사용하게 된다.

결국, 서버에서 온 응답값을 함수 밖에서 사용한다는 뜻인데 문득 이런 궁금증이 들었다.

"함수 밖으로 값을 뺀다는 것은 함수의 반환값으로도 할 수 있지 않을까?"

 

이번 글에서는 네트워킹 코드에서 반환값을 사용할 경우의 문제점과 함께 `@escaping 클로저`에 대해 알아보도록 하겠다.

참고로 네트워킹 코드는 Alamofire를 기준으로 작성되었다.


반환값으로 네트워킹 코드 작성시 문제점

백문이 불여일견이라고, 원래 `completionHandler`와 `@escaping 클로저`를 통해 작성되어있는 네트워킹 코드를 작성해보겠다.

아래 코드는 원래 방식의 코드이다.

func callKakaoBookAPI(query: String, page: Int, completionHandler: @escaping (Book) -> Void ) {
    let url = "https://dapi.kakao.com/v3/search/book?query=\(query)&size=20&page=\(page)"
    let header: HTTPHeaders = [
        "Authorization": APIKey.kakao
    ]

    AF.request(url, method: .get, headers: header)
      .responseDecodable(of: Book.self) { response in

        switch response.result {
        case .success(let value):
            completionHandler(value)
        case .failure(let error):
            print(error)
        }
    }
}

코드의 동작은 간단하다. url을 통해 kakaoAPI를 request하고, 그 응답값을 Book이라는 구조체에 담아 completionHandler를 통해 함수 밖으로 escaping 시키고 있다.

 

해당 코드를 사용할때는 그래서 다음과 같이 클로저의 형태로 사용하게 된다.

func callRequest(query: String) {
    NetworkManager.shared.callKakaoBookAPI(query: query, page: page) { value in
        self.list = value.documents
    }
}

 

 

그렇다면 Book타입의 응답값을 다음과 같이 함수의 반환값에 담아 사용할 수도 있지 않을까?

func callKakaoBookTest(query: String, page: Int) -> Book {
    let url = "https://dapi.kakao.com/v3/search/book?query=\(query)&size=20&page=\(page)"
    let header: HTTPHeaders = [
        "Authorization": APIKey.kakao
    ]
    
    AF.request(url, method: .get, headers: header)
        .validate(statusCode: 200..<500)
        .responseDecodable(of: Book.self) { response in
            
        switch response.result {
        case .success(let value):
            print("✅SUCCESS")
            return value
        case .failure(let error):
            print(error)
        }
    }
}

 

그럼 사용할때도 다음과 같이 간단하게 사용할 수 있을것 같았다.

func callRequest(query: String) {
    let result = NetworkManager.shared.callKakaoBookTest(query: query, page: page)
    list = result.documents
}

 

 

그런데, 반환값으로 구성된 코드를 작성하자마자 다음과 같은 오류가 발생했다.

 

현재 이 함수는 Book이라는 타입을 반환하는 함수이다.

그래서 AF구문에서 반환하는 값 이외에도 어쨌든 Book이라는 타입을 반환해야 하는 상황이라는 것이다.

 

일반적으로 네트워킹 코드는 시간이 오래걸릴 수 있기 때문에 메인쓰레드가 아닌 글로벌 쓰레드에서 동작하게 된다.

그래서 AF구문은 `callKakaoBookTest()`메서드보다 늦게 끝나게 된다.

즉, 네트워크에서 응답값이 오기 전에 메서드의 생명주기가 먼저 끝나게 되므로, 응답값과는 상관 없는 `what`라는 이상한 값이 return될 수 밖에 없는 것이다.

 

지금의 상황을 그림으로 표현하자면 다음과 같다.

네트워킹이 종료되기 전에 함수가 먼저 종료됨으로, 네트워킹의 응답과 상관없는 이상한 값이 return되는 문제가 발생한다.

그렇다면 @escaping은 어떤 녀석이길래 네트워킹의 응답값을 성공적으로 반환할 수 있던 것일까?


@escaping 클로저

개발자문서에 따르면 @escaping 클로저에 대한 설명은 다음과 같다.

클로저가 함수에 인수로 전달되지만 함수가 반환된 후에 호출될 때 클로저는 함수를 탈출한다고 합니다. 클로저를 매개변수 중 하나로 사용하는 함수를 선언할 때 매개변수의 유형 앞에 @escaping을 작성하여 클로저가 탈출할 수 있음을 나타낼 수 있습니다.

 

@escaping 클로저는 함수가 반환된 이후에 호출되는 동작을 한다고 볼 수 있다. 그리고 반환값은 함수 외부에서 정의된 변수에 저장된다.

즉, 함수는 작업을 마친 후 어떤 값을 반환하지만, 작업이 완료될때 까지 클로저가 호출되지 않고, 함수 외부에서 매개변수를 통해 저장된 반환값을 사용할 수 있는 것이다.

 

이러한 특징 덕분에 네트워킹 코드에서 함수의 생명주기와 관계없이 네트워크의 응답값을 사용할 수 있었던 것이며, 꼭 네트워킹 코드가 아니더라도 비동기 작업을 하는 함수들은 completionHandler로 @escaping 클로저 인수를 사용한다.

 


 

※ 참고자료

- https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

- https://janechoi.tistory.com/6