[iOS] @escaping Closure(탈출 클로저)에 대한 고찰 with Network

eung7_·2022년 5월 13일
3

iOS

목록 보기
15/17
post-thumbnail

@escaping Closure(탈출 클로저)!

@escaping이란 키워드가 파라미터 타입 앞에 붙으면 함수가 끝난 이후에도 클로저를 실행할 수 있다.
그런 이 클로저는 요긴하게 쓰이는 부분은 Network 작업을 할 때 종종 사용한다.
왜냐하면 콜백 함수로 비동기 작업을 수행할 때에는 함수의 동작이 이미 끝난 상태이므로..

클로저는 동적 메모리 할당으로 Heap영역에 메모리가 할당된다. 그래서 함수가 끝나도 클로저는 계속 참조할 수 있는 것이다.
물론 이 부분에서 순환 참조에 대해 신경써줘야할 부분이 있지만, 이것은 지금 논외로 하겠다.
순환 참조에 대해 자세하게 알아보고 싶으면 위의 링크를 클릭하면 된다.


network에서 escaping closure

탈출 클로저가 주로 network작업에서 쓰이는 이유는 대표적으로 다음과 같다.

  1. request를 보내고 response를 받을 때에는 클로저로 콜백 함수로써 데이터를 받기 때문
  2. 비동기 작업에서 작업의 순서를 정해주기 위해
func fetchMovies(from term: String, completion: @escaping () -> Void) {
    var components = URLComponents(string: "https://itunes.apple.com/search")!

    let movieName = URLQueryItem(name: "term", value: term)
    let media = URLQueryItem(name: "media", value: "movie")
    let entity = URLQueryItem(name: "entity", value: "movie")
    let limit = URLQueryItem(name: "limit", value: "20")

    components.queryItems = [ movieName, media, entity, limit ]

    let url = components.url!

    var request = URLRequest(url: url)
    request.httpMethod = "GET"

    URLSession.shared.dataTask(with: request) {[weak self] data, response, error in
        guard let self = self else { return }
        if let error = error {
            print(error.localizedDescription)
            return
        }
        
        DispatchQueue.main.async {
            do {
                let object = try JSONDecoder().decode(Result.self, from: data!)
                self.items = object.results

                completion()
            } catch let error {
                print(error.localizedDescription)
            }
        }
    }
    .resume()
}

위에 fetchMovies 메서드는 itunes open API를 통해 json데이터를 받아오는 과정이다.
여기서 중요한 부분은 URLSession을 통해 dataTask를 불러올 때이다.
trailing closure로 콜백 함수로 받아들여온 데이터를 비동기로 작업하는 것이 보일 것이다.

사실 이 dataTask라는 메서드도 당연하게? 하나의 탈출 클로저로 인자로 받고 있다.

    open func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask

fetchMovies 로직 살펴보기

이 메서드의 로직을 순서대로 살펴보자면
URL을 구성하는 Components들을 만들고 URLSession의 싱글톤 객체에 접근하여 dataTask메서드를 불러온다음
그것을 resume 하고 함수는 종료된다.

즉, fetchMovies는 데이터를 받아들여올 때까지 기다려주지 않고 종료된다.

이제 함수의 인자인 completion이라는 클로저를 콜백 함수 안에서 실행시킨다.
만약 여기서 @escaping이란 키워드를 붙여주지 않으면 오류가 뜬다.
당연하게도 dataTask의 콜백함수로 받는 completionHandler는 메서드가 종료되고 비동기 데이터를 가져오기 때문이다.

왜 하필 @escaping 일까?

처음보면 어색한 개념일 것 같은 생소한 개념이지만, 사실은 프로그래머가 원하는데로 데이터를 사용하게 하기 위함이다.
대표적으로 비동기로 받는 데이터의 순서를 정해줄 수 있다.

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    if let text = searchBar.text {
        viewModel.fetchMovies(from: text) {[weak self] in
            self?.collectionView.reloadData()
        }
    }
}

다음 코드는 UISearchBarDelegate를 채택하여 구현한 메서드이다.
방금 정의한 fetchMovies라는 메서드를 불러옴과 동시에 클로저를 정의해주고 있다.
클로저의 로직은 collectionView를 다시 그리는 reloadData메서드를 불러오고 있다.

여기서 방금 말한 2. 비동기 작업에서 작업의 순서를 정해주기 위해 라는 것이 나온다.

만약 탈출 클로저를 쓰지 않는다면, 이렇게 코드를 쓸 수도 있다.

func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
    if let text = searchBar.text {
        viewModel.fetchMovies(from: text)
        collectionView.reloadData()
        }
    }
}

이렇게 되면 문제점이 뭐냐면 데이터를 받아오기전에 reloadData가 호출될 수 있다는 것이다.
결국 network를 사용한 로직은 UI 업데이트와 데이터간의 순서가 중요한데
이것을 해결할 수 있는 것이 탈출 클로저인 것이다.


클로저에 대한 공식 스위프트 도큐먼트는 아래의 링크에서 참조할 수 있다.
https://docs.swift.org/swift-book/LanguageGuide/Closures.html

profile
안녕하세요. iOS 개발자 eung7입니다.

0개의 댓글