SWIFTUI/Grammar

SwiftUI : @escaping

서근 2021. 6. 11. 00:48
반응형

 

@escaping에 대해 알아보도록 합시다.

@escaping 이란?

Escaping클로저는 클로저가 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저입니다.

Non-Escaping클로저는 이와 반대로 함수의 실행이 종료되기 전에 실행되는 클로저입니다.

 

앱을 만들면서 우리는 자주 인터넷에서 데이터를 다운로드해야 합니다. 이때 바로 비동기(Async) 코드를 사용해야 합니다. 보통 함수를 실행할 때는 위에서 아래로 함수가 실행되는데 만약 우리가 데이터베이스로 갈 때에는 인터넷에서 데이터를 다운로드한 뒤에 이 데이터를 바로 앱으로 가져올 수 없습니다. 왜냐하면 먼저 서버로 이동하고 데이터를 가져오기까지 몇 초의 시간이 걸리기 때문이죠. 그렇기 때문에 비동기 처리를 해야 합니다. 그리고 이를 @escaping 클로저로 빠르게 수행하게 되는 것이죠.

 

말로는 이해하기 어렵기 때문에 바로 코드를 작성해가면서 왜 @escaping 클로저가 필요한지 알아보도록 하죠!

 

우선 아주 간단하게 ViewModel 클래스를 생성해서 아래와 같이 구현해줍니다.

import SwiftUI


class EscapingViewModel: ObservableObject {
    @Published var text: String = "안녕하세요"
}
struct EscapingView: View {
    @StateObject var ViewModel = EscapingViewModel()
    
    var body: some View {
        Text(ViewModel.text)
            .font(Font.largeTitle.bold())
            .foregroundColor(.yellow)
    }
}

그리고 ViewModel 클래스에 간단한 함수를 하나 작성합니다.

class EscapingViewModel: ObservableObject {
    @Published var text: String = "안녕하세요"
    
    func getData() {
        
    }
}
struct EscapingView: View {
    @StateObject var ViewModel = EscapingViewModel()
    
    var body: some View {
        Text(ViewModel.text)
            .font(Font.largeTitle.bold())
            .foregroundColor(.yellow)
            .onTapGesture {
                ViewModel.getData()
            }
    }
}

이제 "안녕하세요"라는 Text를 클릭하면 다른 함수가 실행될 수 있도록 함수를 실행할 수 있도록 해주겠습니다. 또 다른 함수로는 func downloadData()를 생성하려고 하는데 이 함수는 String을 반환할 수 있도록 해주겠습니다.

    func downloadData() -> String {
        return "서근블로그 입니다."
    }

downloadData()에 함수를 작성해줬으니 이제 getData()에 호출해줘야겠네요.

class EscapingViewModel: ObservableObject {
    @Published var text: String = "안녕하세요"
    
    func getData() {
        let newData = downloadData()
        text = newData
    }
    
    func downloadData() -> String {
        return "서근블로그 입니다."
    }
}

캔버스를 실행해서 "안녕하세요"라는 텍스트를 터치해보면 정상적으로 "서근 블로그입니다"라는 텍스트로 바뀌는 것을 확인할 수 있습니다.

 

여기서 한 가지 알아야 할 점은 바로 downloadData()에 있는 함수를  synchronized코드라고 합니다. 기본적으로 만약 우리가 이 함수 안에 많은 로직을 가지고 있다면 이 함수는 위에서부터 차례대로 실행되죠.

이런 식으로 한 줄씩 코드가 실행되고 바로 return하게 됩니다.

 

계속해서 아래에 또 다른 downloadData()함수를 생성하고  동일하게 String을 반환하는 동시에 textreturn해주겠습니다. 

    func getData() {
        let newData = downloadData2()
        text = newData
    }
    
    func downloadData() -> String {
        return "서근블로그 입니다."
    }
    
    func downloadData2() -> String {
        return "서근블로그 입니다."
    }

downloadData2()함수가 실제로 인터넷에서 데이터를 다운로드해서 데이터베이스에 이동한다고 가정했을 때, 약간의 지연이 있을 것입니다. 지금은 인터넷에서 데이터를 다운로드하지 않을 거기 때문에 이 함수 안에 DispatchQueue를 사용하여 2초의 지연을 주도록 하죠.

 

전 게시글에서 몇 번 사용해왔던 DispatchQueue.main.asyncAfter(deadline: .now() + 2.0) {} 코드 안에 return "서근 블로그입니다" 를 넣어주고 실행해보려고 하면 한 가지 에러가 발생합니다.

앞서 말했다시피 downloadDataString을 즉시 반환하고 있고 이것을 synchronized코드라고 한다고 했죠? 간단하게 말해 동기화라는 뜻입니다. 하지만 우리가 asyncAfter코드를 넣어주게 되면서 이 함수는 비동기가 되고 그렇기 때문에 함수가 즉시 발생하지 않습니다.  

 

인터넷에서 데이터를 다운로드해서 가져올 때 항상 위와 같은 문제에 부딪힐 것이고 우리는 이것을 사용할 수 없게 됩니다. 

 

즉, 비동기 작업을 시작하는 많은 함수는 클로저 인수를 completion handler로 사용합니다. 함수는 작업을 시작하고 나서 바로 return을 하지만 그 클로저는 작업이 완료되기 전까지 불리지 않는 것이죠. 작업을 완료하기 위해서는 탈출을 해야 하고 나중에 호출돼야 합니다.

 

이를 해결할 수 있는 해결방법에 대해 알아보도록 하겠습니다. @escaping클로저에 대해 알아보기 전에 기본적으로 클로저에 대해 알아보도록 하겠습니다. 

 

func downloadData2()에서 -> String을 반환하는 대신에 매개변수를 사용해보겠습니다.

 

매개변수 타입의 이름을 completionHandler라고 지정해주고 유형은 (_ data: String))으로 작성 후 -> VoId를 반환합니다.

 

여기서 completionHandler에 대해 알아두는 것이 좋은데 간단히 말해서 "어떠한 일이 끝났을 때 진행할 업무를 담당" 한다고 생각하시면 됩니다. 

 

자세한 내용은 이곳에서 확인해 보시는 것을 추천드립니다.

    
    func downloadData2(completionHandler: (_ data: String) -> Void) {
        completionHandler("서근블로그 입니다.")
    }

함수를 수정하고 코드를 보면 상단에 에러 메시지를 확인할 수 있는데, 우리는 downloadData2()에 아무것도 반환하지 않고 있죠? 그렇기 때문에,

이곳에 변수를 설정할 수 없습니다. 그래서 위 코드를 대신해 아래와 같이 호출해줘야 합니다.

엔터를 눌러볼게요.

실제로 downloadData2Stringdata가 됩니다. 

    func getData() {
        downloadData2 { returnedData in
            text = returnedData
        }
    }

캔버스를 실행해보면 정상적으로 "서근 블로그입니다"가 호출됩니다.

계속하기 전에 한 가지 알아봤으면 하는 것이 있는데, 저는 아직 (_ data: String) -> Void)를 사용하는데 익숙하질 않습니다. 그래서 간단한 예제를 가지고 작성하면서 이해해볼까 합니다.

 

 

 

    func doSomething(data: String) {
        print(data)
    }

doSomething함수는 위와 같이 String 타입을 가지고 있는 data입니다. 이것을 호출하면 아래와 같죠.

지금 doSomething함수에는 하나의 내부 매개변수 레이블을 가지고 있습니다. 하지만 외부 매개변수 레이블을 정해줄 수 도 있습니다.

    func doSomething(another data: String) {
        print(data)
    }

이렇게 another이라는 외부 매개변수 레이블을 사용하고 내부 매개변수 레이블은 함수 내부에서만 사용되게 됩니다. 즉, 호출할때는 기독성이 좋게 외부매개변수레이블 사용, 함수내에서는 data라는 내부매개변수레이블 사용 (함수 내에서는 외부 매개변수 레이블 사용 X)

 

이제 매개변수 유형을 -> Void  즉, 매개변수를 아무것도 반환하지 않도록 해줍니다. 아무것도 반환하지 않을 때 Void를 사용해도 되지만 또 다른 방법은 단순히 ( ) 를 넣어줄 수 도 있습니다.

    func doSomething(another data: String) -> Void {
        print(data)
    }
    
    
    func doSomething(another data: String) -> () {
        print(data)
    }

 

 

자 이제 다시 돌아가서, downloadData3을 하나 더 생성해주겠습니다. 이 함수에는 아까 사용했던 지연 설정을 해주도록 하겠습니다.

    func downloadData3(completionHandler: (_ data: String) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completionHandler("서근블로그 입니다.")
        }
    }

아까와 동일하게 에러가 났죠?

해석 : "이스케이프 클로저는 비 이스케이프 매개 변수 'completionHandler'를 캡처합니다."

non-escaping클로저를 Escaping하도록 수정하려면 아래와 같이 함수를 수정해야 합니다.

    func downloadData3(completionHandler: @escaping (_ data: String) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            completionHandler("서근블로그 입니다.")
        }
    }

이제 이 함수는 Async 즉, 비동기 함수가 되었습니다. 이제 이 함수를 getData()에 호출해주겠습니다.

    func getData() {
        downloadData3 { returnedData in
            text = returnedData
        }
    }

getData()함수를 보면 하나의 에러 메시지가 있는데,

해석 : "클로저에서 속성 '텍스트'를 참조하려면 캡처 의미를 명시 적으로 만들기 위해 'self'를 명시 적으로 사용해야 합니다."

라네요. 

 

자, 실제로 인터넷에서 데이터를 다운로드하려면 비동기 작업을 수행할 때마다 이 오류가 발생하게 될 것입니다. 이문제를 해결하는 가장 쉬운 방법은 self를 추가해주는 것이죠.

self.text = returnedData

그런데 실제로 이 코드에서 selfstrong reference 즉, 강한 참조 입니다. 

    func getData() {
        downloadData3 { returnedData in
            self.text = returnedData
        }
    }

실제로 이 코드는  즉시 실행되지 않을 것입니다. 왜냐하면 이 클로저는 코드에서 호출할 때마다 실행되므로 이 핸들러는 2초 의 지연 후에 실행되게 됩니다. 그리고 downloadData3가 호출되고 이 클로저가 실행되는 사이에 앱을 사용하는 사용자가 앱을 나가거나, 앞으로 가거나 다른 행동을 할 가능성이 있습니다. 즉, class가 초기화될 수 도 있는 것이죠. 하지만 강한 참조 self는 이 클래스가 계속 살아있어야 합니다. 이를 해결하기 위해서는 약한 참조 로 바꿔줘야 합니다.

 

weak self를 추가하게 되면 self는 실제로 옵셔널 상태가 됩니다.

    func getData() {
        downloadData3 { [weak self] returnedData in
            self?.text = returnedData
        }
    }

이제 다시 캔버스를 실행해보면 2초의 지연 후에 "서근 개발 블로그입니다" 라는 텍스트가 정상적으로 나오게 됩니다.

 

게시글을 끝마치지 전에 위에 사용한 코드보다 더 유용하고 쉽게 탈출 하는 방법이 있습니다. 우선 downloadData3를 복사한 후 이름은 downloadData4로지정한 후에 이 반환된 데이터에 대한 모델을 만들어주겠습니다.

    struct DownloadResult {
        let data: String
    }

그리고 이 구조체를 downloadData4data: String을 지워준 뒤 DownloadResult를 넣어줍니다. 그리고 함수를 아래와 같이 수정해줍니다.

    func downloadData4(completionHandler: @escaping (DownloadResult) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let result = DownloadResult(data: "서근블로그 입니다")
            completionHandler(result)
        }
    }

이제 이 함수를 호출해야 하기 때문에 getData()로 돌아가서 함수를 다음과 같이 수정해주겠습니다.

    func getData() {
        downloadData4 { [weak self] returnedResult in
            self?.text = returnedResult.data
        }
    }

반환하고자 하는 데이터가 너무 많다면  이 방법이 위에 방법보다 더 효율적이라는 것을 알 수 있습니다. 

 

그렇다면 지난 게시글에서 다뤘던 typealias를 사용해볼까요?

 

 

downloadData4()를 복사해서 이름은 downloadData5()라고 설정해주고 아래에 typealias를 작성해줄게요.

typealiastype name에는 새로운 타입의 이름을 사용자와 해주고, type expression(DownloadResult) -> Void를 가져와 주겠습니다. 

typealias DownloadCompletion = (DownloadResult) -> Void

//혹은 
//typealias DownloadCompletion = (DownloadResult) -> ()

그리고 download5() 함수의 매개변수 타입 부분을 다음과 같이 수정해줄 수 있습니다.

    func downloadData4(completionHandler: @escaping (DownloadResult) -> Void) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let result = DownloadResult(data: "서근블로그 입니다")
            completionHandler(result)
        }
    }
    
    
    func downloadData5(completionHandler: @escaping DownloadCompletion) {
        DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
            let result = DownloadResult(data: "서근블로그 입니다")
            completionHandler(result)
        }
    }

자 어떤가요? 위 코드의 downloadData4함수 코드보다 downloadData5의 코드가 더 깔끔해졌죠? 

 

 

이렇게 해서 SwiftUI에서 @escaping을 사용하는 방법에 대해 알아봤습니다. 여전히 어렵지만 다른 예제들을 보면서 더 공부해봐야겠네요 :)

 

 

읽어주셔서 감사합니다🤟