-
Completion Handler / ClosureiOS/iOS개념정리 2022. 5. 15. 15:32
클로저란
우리는 함수를 정의할떄 func 이라는 키워드를 이용해 정의해왔다.
클로저는 함수와 동일한 기능을 하되, func키워드와 함수명이 없는 함수이다.
흔히들 익명함수라고 부르기도 한다.
클로저의 기본 문법에 대해 먼저 살펴보도록 하자.
{ (parameters) -> Return Type in body }
() 사이에 인자를 받고 ->를 통해 반환 타입을 명시한다.
이후 in 키워드 뒤에 나오는 부분이 실행할 코드를 기재하는 클로저의 몸체 부분이다.
Func vs Closure
함수와 클로저의 차이점에 대해 간단히 살펴보도록 한다.
Function
- func 키워드를 통해 정의한다.
- 이름을 갖는다.
- in 키워드가 존재하지 않는다.
Closure
- func 키워드가 존재하지 않는다.
- 이름을 갖지 않는다.
- in 키워드를 통해 인자 & 반환타입과 몸체를 분리한다.
클로저는 iOS 개발시 상당히 많은 부분에서 응용되어 사용된다.
가독성이 좋기에 다양한 축약형이 존재한다.
간단히 축약형들에 대하여 살펴보도록 한다.
1. return 키워드 생략
var Hello = { (name: String) in return "Hello \(name)"} var Hello = { (name: String) in "Hello \(name)"}
인자를 전달받으면 해당 Hello (인자)를 출력하는 클로저이다.
첫번째 코드에는 return 키워드가 사용되었으나 두번째 코드에서는 return 문이 생략되었다.
이렇듯, 클로저에서는 return 키워드를 생략할 수 있다.
2. 매개변수 타입 생략
var Hello: (String) -> String = {"Hello \($0)"}
변수의 타입을 명시해주면 매개변수의 타입 또한 생략이 가능하다.
클로저는 다양한 용도로 사용된다.그 중 대표적인 용도가 전달인자로서의 사용이다.
전달인자로서의 사용
let add: (Int, Int) -> Int = add = {$0 + $1} let sub: (Int, Int) -> Int = sub = {$0 - $1} let div: (Int, Int) -> Int = div = {$0 / $1} let mul: (Int, Int) -> Int = mul = {$0 * $1} func calc(a:Int, b:Int method:(Int, Int) -> Int) -> Int { return method(a,b) } print(calc(a:4, b:2, method:add)) // 6 print(calc(a:4, b:2, method:sub)) // 2
위와 같이 클로저는 함수의 전달인자로 사용될 수 있다.
후행 클로저
만일, 클로저가 함수의 마지막 전달인자이거나 전달인자가 클로저 하나 뿐이면 전달인자로 넘기지 않고 이를 함수 호출후 중괄호를 통해 클로저를 구현하여 인자 전달이 가능하다. 이를 후행 클로저라고 부른다.
result = calculate(a:3, b:2) { (left:Int, right:Int) -> Int in return left + right } // 위 코드는 아래와 같이 축약이 가능하다. result = calculate(a:3, b:2) {$0 + $1}
이렇게 간단히 클로저의 기본 문법과 축약형에 대해 알아보았다.
이제는 보다 심화된 내용에 대하여 정리해보도록 한다.
Closure 는 Reference Type
기본적으로 클로저는 Reference Type이다.
앞서 우리는 클래스와 구조체는 각각 Reference Type & Value Type 이라고 배웠던 기억이 있다.
맞다, 클래스를 공부할적에 배웠던 Reference Type과 동일한 의미다.
즉, Call By Reference 방식으로 객체를 가리키고 있는 메모리의 주소값을 복사해오는 방식이다.
매개변수로 클로저에서 사용되는 매개변수는 값을 복사하는게 아니라 해당 값을 참조하여 사용하게 된다.
말로 표현하니 필자 또한 잘 와닿지가 않는다..
코드로 표현해보자!
var a:Int = 1 var b:Int = 0 var closure = {print(a,b)} closure() // 1, 0 a = 0 b - 1 closure() // 0, 1
위와 같이 클로저 내부의 a,b는 외부의 a,b값을 CBV 방식으로 참조하여 가져온다.
이러한 방식으로 값을 참조하게 된다면 외부에서 값이 변경되면 참조하고 있는 값 또한 즉각 변경된다.
꼭 명심하자, 클로저는 기본적으로 Reference Type이다!
Capture List
이것 또한 너~~~~~~~~~~~~무나도 중요하다...!
예시 코드나 오픈소스들을 살펴보면 CaptureList가 상당히 많이 쓰였다!
Capture List 라는 합성어는 이름 그대로 정보를 캡처해버리는것을 의미한다.
아래 예시 코드를 살펴보도록 하자!
var index = 0 var closureArr: [() -> ()] = [] for _ in 1...5 { closureArr.append({print(index)}) index += 1 } for i in 0..<closureArr.count { closureArr[i]() }
위 코드의 예상 아웃풋을 곰곰히 생각해보자!
위 코드의 아웃풋은 0, 1, 2, 3, 4 가 아닌 5, 5, 5, 5, 5 다.
왜냐하면 closureArr.append()에서 변수 index가 루프를 돌면서 최종 5가 되는데, 클로저는 변경된 index값 5를 참조하기 때문이다.
이러한 예상치 못한 결과를 방지하고자 사용하는 것이 Capture List이다.
var index = 0 var clousreArr:[() -> ()] = [] for _ in 1...5 { closureArr.append({[index] in print(index)}) index += 1 } for i in 0..<closureArr.count { closureArr[i]() }
위 코드의 결과값은 0, 1, 2, 3, 4다.
이전 코드와 다른점은 클로저 내부에 참조 변수 index를 [index]로 표기한 것이다.
이렇게 참조하고자 할 변수에 대괄호를 붙여주는 것이 캡처 리스트를 사용하겠다는 명시를 의미한다.
캡처 리스트를 사용시, 캡처 시점은 클로저가 생성될 때 캡처를 진행한다.
즉, index가 참조하고 있는 값을 캡처하고 클로저 내부에 저장하여 값에 접근하겠다는 의미이다.
더욱 간단한 예시를 통해 확실한 이해를 돕자!
var c = 0 var d = 0 let smartClosure: () -> () = { _ in print(c,d)} smartClosure() // 0, 0 c = 6 d = 9 let smartClosure: () -> () = { [c,d] in print(c,d)} smartClosure()
위와 같은 코드가 있다고 가정해보자.
위 코드에서 마지막으로 호출한 클로저의 결과값은 0,0 이다.
in 키워드 앞에 []를 통해 변수를 감싸주면 클로저는 더이상 기존 변수를 참조하지 않는다!
대신 클로저는 이를 복사하여 클로저 내에 자체 복사본을 만들어 사용한다!
결과적으로, 기존 변수의 값이 변경되더라도, 클로저는 더이상 참조하지 않기 때문에 전혀 상관하지 않는 독립군이다.
단, 캡처 리스트 내 명시된 요소가 참조 타입이 아닌 경우에만 클로저가 생설될 떄 캡처를 진행한다.
만일 참조 타입이라면 클로저가 호출될 때 캡처를 진행한다.
참고자료
요약
- 클로저는 func, 이름이 없는 익명 함수다.
- 다양한 축약형이 존재한다.
- 기본적으로 클로저는 Reference Type이다.
- 캡처 리스트는 참조하고 있는 값을 클로저 실행시 캡처하여 사용하고자 할 때 사용하는 기능이다.
Trailing Closure
Trailing Closure, 후행 클로저는 함수의 마지막 인자로 클로저 표현식을 함수에 전달하거나 클로저 표현식이 긴 경우에 사용한다. (매우 꿀 기능이다!)
아래 코드 예시를 살펴보도록 하자.
func travel(action: () -> Void) { print("I'm getting ready to go.") action() print("I arrived") }
위 travel 메서드는 인자로 action 이라는 Closure를 채택하고 있다,
action은 함수 내부에서 두 번의 프린트 사이에 실행된다.
우리는 위 travel 메서드를 아래와 같은 방식으로 사용할 수 있다.
travel() { print("I'm driving in my car") }
위 예시의 경우에는 인자가 필요없기 때문에 인자 표현을 위한 () 또한 생략이 가능하다.
travel { print("I'm driving in my car") }
Completion Handler?
자 이제는 Completion Handler가 무엇인지 살펴보도록 한다.
사용자가 앱을 사용하는 동안에 앱을 업데이트를 진행하고 개발자는 업데이트가 끝나면 이를 반드시 사용자에게 알려야 하는 상황이라고 가정해보자.
개발자가 업데이트가 끝났음을 알리기 위해 내부에는 "업데이트가 정상적으로 진행되었습니다" 라는 문구가 작성되어 있는 alert창을 띄운다.
이렇듯 Completion Handler는 어떠한 일이 끝났을 떄 진행할 업무를 담당한다.
Introduce
아래 코드는 모두가 익숙할만한 다음 ViewController로 화면을 전환하는 코드다.
import UIKit let firstVC = UIViewController() let nextVC = UIViewContrller() firstVC.present(nextVC, animated: true, completion: nil)
present 메서드 마지막 파라미터 completion에 주목하자.
completion 파라미터에는 ()->() 또는 ()->Void 타입의 클로저 블락을 사용할 수 있다.
아래와 같이 completion 파라미터에 클로저를 적용시켜본다.
firstVC.present(nextVC, animated: true, completion: { () in print("화면전환 완료")})
위 메서드가 정상적으로 실행되면 nextVC가 팝업되면서 콘솔창에는 "화면전환 완료" 라는 문자열이 출력된다.
위 코드는 아래와 같이 축약 또한 가능하다.
firstVC.present(nextVC, animated: true, completion: { print("화면전환 완료")})
인자값이 없으니 이를 생략하고 in 키워드 또한 생략이 가능하니 이 또한 생략했다!
놀라운건 이를 한번 더 축약할 수 있다는 점이다.
firstVC.present(nextVC, animated: true) { print ("화면전환 완료")}
앞서 간단히 살펴봤던 Trailing Closure를 이용한 축약법이다.
Completion Handler 디자인
이제는 위 예시 present 메소드처럼 디자인을 해보자.
일이 끝나면 사장님께 퇴근하겠다는 얘기를 전달하는 클로저를 작성해본다!
let handleBlock: (Bool) -> Void = { doneWork in if doneWork { print("퇴근하겠습니다") } } handleBlock(true) // 퇴근하겠습니다
이를 $ 사인을 이용해 인자를 처리해보도록 한다.
let handleBlock: (Bool) -> Void = { if $0 { // replace $0 = doneWork print("퇴근하겠습니다") } }
Completion Handler를 통한 데이터 전달
Alamofire와 같은 통신 라이브러리를 이용해 JSON 형태의 데이터를 받아올 떄 아래와 같은 코드 형태를 많이 살펴봤을 것이다.
Alamofire.request("https://google.com").responseJSON { response in print(response) }
메서드의 request가 서버로 전송되어 이에 대한 결과값이 response 인자에 담겨 이를 클로저 내부에서 사용할 수 있게된다.
이렇듯 Swift 를 이용하다보면 함수의 마지막 인자로 Closure를 적용하는 경우를 심심치않게? 살펴볼 수 있다.
이는 함수가 종료된 직후(Called at the back)의 이벤트 처리를 위해 매우 많이 사용된다.
위 개념은 꼭 반복하여 익히도록 하자.
(Completion Handler를 통한 데이터 전달 _ 추가)
본 게시글은 How To: Pass Data Between View Controllers in Swift를 바탕으로 작성하였습니다.
여러분의 앱이 여러 개의 사용자 인터페이스(UI)를 가지고 있다면 여러분은 하나의 UI에서 다른 UI로 데이터를 전달해야 하는 경우도 생길 것입니다. Swift에서는 View Controller 사이에 어떤 방법으로 데이터를 전달할 수 있을까요?
뷰 컨트롤러(View Controller) 사이에 데이터를 주고 받는 것은 iOS 개발의 중요한 일부입니다. 여러분은 몇 가지 방법으로 이를 해낼 수 있고 각기 다른 이점과 약점을 가지고 있습니다.
뷰 컨트롤러 사이에 쉽게 데이터를 교환하는 방법을 선택하는 것은 여러분이 앱 구조를 어떻게 할 것인지에 달려 있습니다. 앱의 구조(App architecture)는 여러분이 뷰 컨트롤러 사이에 어떻게 작동이 이루어 질 것인지에 영향을 주고, 반대로 여러분이 뷰 컨트롤러 사이에 데이터 교환을 어떻게 할 것인지에 따라 앱의 구조가 달라집니다.
Swift에서 여러분은 뷰 컨트롤러 사이에 다음의 6가지 방법으로써 데이터를 주고 받을 수 있습니다.
- 인스턴스 프로퍼티(property)를 사용하는 방법(A → B 방향)
- 스토리보드(Storyboard)와 세그웨(segue)를 사용하는 방법
- 인스턴스 프로퍼티와 함수를 사용하는 방법(A ← B 방향)
- 델리게이션(delegation) 패턴을 사용하는 방법
- 클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
- NotificationCenter 또는 Observer 패턴을 사용하는 방법
이 게시글에서 여러분은 뷰 컨트롤러 사이에 데이터를 주고 받는 6가지의 각기 다른 방법에 대해 익히게 될 것입니다. 이 방법에는 프로퍼티(property)를 사용하는 방법, 세그웨(segue)를 사용하는 방법 및 NSNotificationCenter를 사용하는 방법도 들어 있습니다. 비교적 간단한 이들 방법을 통해 좀 더 복잡한 응용까지 나아갈 수 있습니다. 준비가 되었다면 이제 시작하겠습니다.
클로저(closure) 또는 핸들러(completion handler)를 사용하는 방법
클로저(closure)를 사용하여 뷰 컨트롤러 사이에 데이터를 전달하는 것은 프로퍼티나 델리게이션을 사용하는 것과 크게 다르지 않습니다. 클로저를 사용하면 상대적으로 사용하기가 쉽고 또한 별도의 메소드(함수)나 프로토콜을 선언하지 않아도 지역적으로 데이터 교환 구문을 정의할 수 있다는 이점이 있습니다.
클로저를 사용하여 뷰 컨트롤러 사이에 데이터를 주고받을 수 있다.
나중에 등장하게 될 뷰 컨트롤러에 다음과 같이 프로퍼티를 하나 만드는 것으로 소스 코드를 시작합니다.
// Swift var completionHandler: ((String) -> Int)?
// Objective-C @interface ... @property (atomic, weak) NSInteger (^completionHandler)(NSString *); @end @implementation ... @synthesize completionHandler; @end
여기서 completionHandler는 클로저 타입을 가지고 있습니다. 이 클로저는 옵셔널(optional)이기 때문에 끝에 ?가 붙습니다. 또한 클로저의 시그니처(signature)는 (String) -> Int입니다. 이것은 이 클로저가 String형 매개변수를 하나 받고 Int형 값을 반환하는 메소드(함수 또는 람다식)임을 뜻합니다.
나중에 등장할 뷰 컨트롤러에서 작성할 또 하나의 사항은 버튼이 탭(tap)되었을 때 그 클로저를 호출하는 것입니다.
// Swift @IBAction func onButtonTap(sender: Any) { let result = completionHandler?("FooBar") print("completionHandler returns ... \(result)") }
// Objective-C -(IBAction) onButtonTapWithSender:(id)sender { NSInteger result; if ([self completionHandler] != nil) { result = [self completionHandler](@"FooBar"); NSLog("completionHandler returns ... %d", result); } }
제시된 예제는 다음과 같이 작동합니다.
한 개의 매개변수가 지정되면서 completionHandler 클로저가 호출되면, 그 클로저의 호출 결과가 result 변수에 보관됩니다.
결과는 print()의 호출에 의해 출력됩니다.
그 다음 MainViewController에서 여러분은 다음과 같이 클로저를 정의합니다.
// Swift secondaryViewController.completionHandler = { text in print("text = \"\(text)\"") return text.characters.count }
// Objective-C [[self secondaryViewController] setCompletionHandler: ^(NSString * text) { NSLog("text = \"%@\", text); return (NSInteger)[text length]; }
이것이 클로저 그 자체입니다. 이것은 지역 범위에서 선언되었기 때문에 클로저가 선언된 스코프 내의 로컬 변수, 프로퍼티 및 메소드(함수)들을 모두 사용할 수 있습니다.
text 매개변수가 출력되어 나오는 클로저에서는 실행의 결과로서 문자열의 길이가 반환됩니다. 여기서 흥미로운 사실이 발견됩니다. 이런 식으로 작성되는 클로저는 뷰 컨트롤러 사이에서 데이터를 양방향으로 전달해준다는 것입니다. 여러분이 클로저를 정의하고 유입될 데이터에 대하여 작업한 다음 이 클로저를 호출한 코드에게 결과 데이터를 반환할 수 있습니다.
물론 여러분은 델리게이션을 사용하거나 프로퍼티 자체를 사용한 메소드(함수) 호출에서도 그 코드 블록을 호출해 준 코드에게 소정의 값을 반환할 수 있다는 것을 알고 있을 지 모릅니다. 전적으로 맞습니다. 다음과 같은 상황에서 클로저는 여러분의 손에 잡히게 될 것입니다.
(1) 여러분은 단지 짧고 간략한 내용의 함수를 작성하고자 굳이 프로토콜을 만들어가며 완전한 델리게이션 방식으로 접근하기를 원하지 않을 수 있습니다.
(2) 여러분은 여러 클래스에 걸쳐 하나의 클로저를 전달하고 싶어질 수도 있습니다. 클로저가 없었다면 여러분은 쏟아지는 함수 호출들을 만들었어야 하겠으나, 클로저와 함께 여러분은 한 블록의 코드만 전달하면 됩니다.
(3) 어떤 데이터가 지역적으로만 존재할 때 여러분은 클로저 안에서 그 데이터에 접근할 수 있는 수준에서 코드 한 블록을 지역적으로(locally) 정의할 필요가 있을 것입니다.
클로저를 사용하여 뷰 컨트롤러 사이에 데이터 전달이 이뤄지게 할 때의 손해는, 여러분의 코드가 다소 복잡해 보일 수 있다는 것입니다. 다른 방법을 사용할 때보다 클로저를 쓰는 것이 더 이치에 맞다고 여겨질 때, 뷰 컨트롤러 사이에 데이터를 전달할 때에 한하여 클로저를 사용하는 것이 현명한 선택입니다.
그렇다면 두 개의 뷰 컨트롤러 사이에 연결고리가 형성되지 않았거나 형성될 수 없음에도 데이터를 주고 받기 위해서는 어떻게 해야 할까요? 다음 장에서 살펴보겠습니다.
출처: https://blog.codingcat.kr/133
Escaping Closure
정의
Escaping 클로저는 클로저가 함수의 인자로 전달됐을 때, 함수의 실행이 종료된 후 실행되는 클로저 입니다. Non-Escaping 클로저는 이와 반대로 함수의 실행이 종료되기 전에 실행되는 클로저 입니다.
Non-Escaping Closure
그럼 이 클로저(closure)는 어떤 클로저 일까요?
func runClosure(closure: () -> Void) { closure() }
클로저가 실행되는 순서를 보면
- 클로저가 runClosure() 함수의 closure 인자로 전달됨
- 함수 안에서 closure() 가 실행됨
- runClosure() 함수가 값을 반환하고 종료됨
이렇게 클로저가 함수가 종료되기 전에 실행되기 때문에 closure는 Non-Escaping 클로저 입니다.
Escaping Closure
이 경우는 어떨까요?
class ViewModel { var completionhandler: (() -> Void)? = nil func fetchData(completion: @escaping () -> Void) { completionhandler = completion } }
클로저가 실행되는 순서를 보면
- 클로저가 fetchData() 함수의 completion 인자로 전달됨
- 클로저 completion이 completionhandler 변수에 저장됨
- fetchData() 함수가 값을 반환하고 종료됨
- 클로저 completion은 아직 실행되지 않음
completion은 함수의 실행이 종료되기 전에 실행되지 않기 때문에 escaping 클로저, 다시말해 함수 밖(escaping)에서 실행되는 클로저 입니다.
escaping 클로저가 사용되는 흔한 예로는 비동기로 실행되는 HTTP Request CompletionHandler이 있습니다.
func makeRequest(_ completion: @escaping (Result<(Data, URLResponse), Error>) -> Void) { URLSession.shared.dataTask(with: URL(string: "http://jusung.github.io/")!) { data, response, error in if let error = error { completion(.failure(error)) } else if let data = data, let response = response { completion(.success((data, response))) } } }
makeRequest() 함수에서 사용되는 completion 클로저는 함수 실행 중에 즉시 실행되지 않고, URL 요청이 끝난 후 비동기로 실행 됩니다. 이 경우에도 completion의 타입에 @escaping 을 붙여서 escaping 클로저라는 것을 명시해줘야 합니다.
보통 클로저가 다른 변수에 저장되어 나중에 실행되거나 비동기로 실행될 때 escaping 클로저가 사용됩니다.
Non-Escaping Closure 와 Escaping Closure
그럼 함수에 @escaping 가 붙은 클로저에는 반드시 escaping 클로저만 인자로 사용해야 할까요?
그렇지는 않습니다. @escaping이 붙어 있어도 다음과 같이 non-escaping 클로저를 인자로 넣을 수 있습니다.
func runClosure(closure: @escaping () -> Void) { closure() // ✅ closure는 non-escaping 클로저이지만 @escaping 사용 가능 }
반대로 escaping 클로저를 @escaping 없이 사용할 수 없습니다.
class ViewModel { var completionhandler: (() -> Void)? = nil func fetchData(completion: () -> Void) { // ❗️@escaping 누락으로 컴파일 에러 발생! completionhandler = completion } }
그렇다면 여기서 한가지 궁금한점이 생깁니다. 🤔
클로저 파라미터 타입에 @escaping을 명시하면 escaping 클로저와 non-escaping 클로저를 모두 사용할 수 있는데, 그러면 그냥 모든 클로저 파라미터 타입에 @escaping 를 붙여서 사용하면 되지 않나?
아니 한걸음 더 나가서 복잡하게 non-escaping, escaping 나누지 말고 전부 escaping으로 통일시키고 따로 @escaping을 명시 하지 않아도 Swift에서 모든 클로저의 파라미터 타입을 escaping으로 간주해서 처리하면 될 것 같은데. 뭐하러 escaping, non-escaping을 나누는거지?
이렇게 escaping, non-escaping 클로저를 나눠서 사용하는 이유는 컴파일러의 퍼포먼스와 최적화 때문입니다.
non-escaping 클로저는 컴파일러가 클로저의 실행이 언제 종료되는지 알기 때문에, 때에 따라 클로저에서 사용하는 특정 객체에 대한 retain, release 등의 처리를 생략해 객체의 라이프싸이클(life-cycle)을 효율적으로 관리할 수 있습니다.
반면 esacping 클로저는 함수 밖에서 실행되기 때문에 클로저가 함수 밖에서도 적절히 실행되는 것을 보장하기 위해, 클로저에서 사용하는 객체에 대한 추가적인 참조싸이클(reference cycles) 관리 등을 해줘야 합니다. 이 부분이 컴파일러의 퍼포먼스와 최적화에 영향을 끼치기 때문에 Swift에서는 필요할 때만 escaping 클로저를 사용하도록 구분해 두었습니다.
'iOS > iOS개념정리' 카테고리의 다른 글
[개념정리] iOS 전반 (면접대비) (0) 2025.01.10 네트워크 비동기와 reloadData의 중요성 (0) 2023.02.05 Timer class vs. GCD DispatchSourceTimer (0) 2022.05.08 [iOS] Delegate, Notification, KVO 비교 및 장단점 정리 (0) 2022.04.20 KVO (프로퍼티 옵저버) (0) 2022.04.06