Search

[Swift 공식 문서] 7. Closures

Closure 란?

전달, 사용이 가능한 코드 뭉치 ( 예: 함수 )
Closure 는 정의된 맥락에서 모든 상수 및 변수에 대한 참조를 capture 하고 저장할 수 있다.
이를 closing over 라고 부릅니다.
Swift 는 capture 에 대한 모든 메모리 관리를 수행합니다. capture 에 대한 자세한 내용은 아래에 서술되어 있으니 너무 걱정하지 마세요
global, nested function 은 closure 의 특별한 예입니다.
클로져는 아래의 세가지 형태를 가집니다.
글로벌 함수 : 이름 존재, 어떠한 값도 capture 하지 않음
중첩 함수 : 이름 존재, 감싸고 있는 함수로부터의 값을 capture
클로져 표현식 : 이름 없음 X , 간단한 구문으로 작성, 그들을 둘러싼 맥락으로 부터 값을 capture
Swift 의 클로져 표현식은 깔끔한 구문, 명확한 스타일이 존재합니다.
맥락내에서 파라미터 및 리턴 값 타입추론
단일 표현식 클로져의 암묵적 리턴
축약된 argument name
Trailing closure 후행 클로져

클로져 표현식 Closure expressions

중첩 함수 내에서 함수 자기 자신이 포함하고 있는 코드 블럭을 이름과 함께 정의하는 것은 편리한 수단이긴 하나 가끔은 함수의 이름 또는 정의 없이 사용하는 것이 더 유용합니다. 클로져 표현식을 사용하면 명확성과 의도를 놓지지 않으면서 축약된 형태로 클로져를 제공할 수 있습니다.

The Sorted Method

Swift 의 표준 라이브러리는 sorted(by:) 메서드를 제공합니다.
이 메서드는 어레이의 값들을 sorting 이라는 파라미터에 전달된 클로져를 기반으로 정렬합니다.
이 메서드는 정렬된 값을 리턴하며, 원본은 변경되지 않습니다.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
Swift
복사
sorted() 메서드는 어레이의 원소 타입과 같은 타입의 인자 두개를 받고 Bool 값을 리턴하는 클로져를 허용합니다. 이 예제에서는 다음(String, String) -> Bool 과 같은 클로져 타입이 요구됩니다. 이 Bool 값은 첫번째 값이 먼저 나와야하면 true 를 리턴해야합니다.
이런 클로져를 메서드에 전달하는 한가지 방법은 올바른 유형의 일반 함수를 작성 , 전달하는 것입니다.
func backward(_ s1: String, _ s2: String) -> Bool { return s1 > s2 //첫번째 값이 더 클 경우 True >> 내림차순 정렬 } var reversedNames = names.sorted(by: backward)
Swift
복사

Closure expression syntax : 구문

1.
in-out 파라미터 사용 가능
2.
초기값 사용 불가
3.
Variadic parameter 로 사용 가능
4.
튜플 또한 파라미터 타입, 리턴타입으로 사용이 가능
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 })
Swift
복사
함수를 따로 정의하지 않았으나, 위 코드는 앞선 함수를 정의하는 방식과 똑같이 작동합니다.
하지만 인자는 더이상 함수가 아닌, inline closure 입니다.

Inferring Type From Context

sorting closure 가 메서드에 인자로 전달되었기 때문에,
Swift 는 sorted() 메서드의 구현사항을 기준으로 , 클로져의 파라미터 타입과 리턴 타입을 추론할 수 있습니다.
따라서 type을 생략할 수 있습니다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } ) // type 생략
Swift
복사
항상 함수 또는 메서드에 클로져를 전달할 때는 타입생략이 가능합니다 생략이 가능하지만 가독성을 위해서 추가하는 경우도 있다고 한다

Implicit Returns from Single Expression Closures

단일 표현식을 갖는 클로져는 암묵적으로 결과를 리턴합니다 따라서
return 키워드 또한 생략이 가능합니다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
Swift
복사

Shorthand Argument Names

Swift 는 클로져에게 자동으로 축약된 인자의 이름을 제공합니다. 인자의 순서대로 $0 $1 $2 ...
따라서 인자의 이름에 대한 정의를 생략할 수 있습니다.
reversedNames = names.sorted(by: { $0 > $1 } )
Swift
복사
$0 은 첫번째 인자를 의미하며,
$1 은 두번째 인자를 의미합니다.

Operator Methods

클로져 표현식을 더 줄이는 방법 또한 있습니다.
만약 정렬하고자 하는 대상이 operator 에의해 연산이 가능하다면 단순히 이 연산자를 건네줌으로서 정렬이 가능합니다.
reversedNames = names.sorted(by: >)
Swift
복사

Trailing Closures : 후행 클로져

만약 함수의 마지막 인자로서 closure 표현식을 전달하고자 하나, 이 표현식이 너무 길 경우, trailing closure 을 사용하는 것이 더 유용합니다.
함수를 호출하고 뒤에 클로져를 작성하면 됩니다.
여러개의 클로져를 모두 후행클로져로 작성할 수 있습니다.
func someFunctionThatTakesAClosure(closure: () -> Void) { // 클로져를 전달 받는 함수. } //일반 클로져 someFunctionThatTakesAClosure(closure: { // closure's body goes here }) // trailing closure 사용 🌟 someFunctionThatTakesAClosure() { // 호출 후 { 중괄호 } }
Swift
복사
위 구문을 활용해서, sorted() 메서드의 호출을 다음과 같이 작성할 수 있습니다.
reversedNames = names.sorted() { $0 > $1 }
Swift
복사
만약 전달받는 인자가 closure 표현식으로 유일하다면, () 을 생략가능
reversedNames = names.sorted { $0 > $1 }
Swift
복사
Trailing closure 는 클로져 표현식이 한줄로 작성하기에는 너무 길 때 유용합니다.
함수가 여러개의 클로져를 인자로 받는다면, 하나의 closure 뒤에 파라미터의 이름과 함께 남은 후행클로져를 작성할 수 잇습니다.
//클로져 두개를 인자로 받는 함수/ func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) { if let picture = download("photo.jpg", from: server) { completion(picture) } else { onFailure() } } // 2개의 클로져 또한 trailing closure 로 작성이 가능하다. loadPicture(from: someServer) { picture in someView.currentPicture = picture } onFailure: { print("Couldn't download the next picture.") }
Swift
복사
여러개의 함수를 요구하는 함수내에서는 이렇게 파라미터를 분리함으로써, 호출시에도 더 명확하게 각 클로져의 역할을 확인할 수 있습니다.

Capturing Values

클로저 캡쳐란 클로저가
매개변수나 지역변수가 아닌 주변 외부의 context를 사용하기 위해 주변 외부의 context를 참조하는 것(Capturing by reference) 입니다.
그래야 주변 외부 context가 없어질지라도 클로저가 주변 외부 context를 사용할 수 있습니다.
캡쳐하는 가장 쉬운방법은 중첩함수를 작성하는 것입니다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int { var runningTotal = 0 func incrementer() -> Int { runningTotal += amount return runningTotal } return incrementer }
Swift
복사
위의 예와 같이 중첩 함수인 incrementer()에서 외부 함수인 makeIncrementer의 매개변수인 amount와 함수 내에서 정의된 runningTotal 변수를 캡처하여 사용하는 것을 볼 수 있습니다. 실제로는 incrementer 함수에는 이러한 변수나 상수가 정의되어 있지 않지만 캡처하여 사용할 수 있는 것입니다. 여기서 makeIncrementer의 반환 타입이 () -> Int인데 반환하는 값이 incrementer이므로 결국에는 incrementer의 반환 타입인 Int형을 반환하게 됩니다.
func incrementer() -> Int { runningTotal += amount return runningTotal }
Swift
복사
incrementer() 함수에는 매개 변수가 없지만 함수 본문 내에서 runningTotalamount를 참조합니다. 이는 surround 함수로부터 amountrunningTotal에 대한 참조를 캡처(Cpaturing by reference)하여 자체 함수 body 내에서 사용하여 수행합니다. 
참조로 캡처(Capturing by reference)하면 makeIncrementer에 대한 호출이 종료 될 때 runningTotalamount가 사라지지 않으며 다음에 incrementer 함수가 호출 될 때 runningTotal을 사용할 수 있습니다.
let incrementByTen = makeIncrementer(forIncrement: 10) incrementByTen() // returns a value of 10 incrementByTen() // returns a value of 20 incrementByTen() // returns a value of 30
Swift
복사
함수가 끝나면 사라지는 게 정상 이지만 캡쳐함으로써 값이 사라지지 않고 유지된다.

Closures Are Reference Types

위의 예에서 본 incrementByTen은 상수로 선언된 값이지만 계속해서 그안의 값이 변한다
함수와 클로져가 Reference type 이기 때문입니다.
함수나 클로져를 상수나 변수에 할당하게되면 상수나 변수에 복사되는 것이 아닌 메모리 주소만 참조합니다.
let alsoIncrementByTen = incrementByTen alsoIncrementByTen() // returns a value of 50 incrementByTen() // returns a value of 60
Swift
복사
따라서 상수로 선언 되었음에도 원본이 변경되는 것을 볼 수 있습니다.
이는 같은 대상을 참조하기 때문입니다.

Escaping Closures

When? : 인자로 전달된 클로저를 함수 밖에서 실행하고자 할 때
비동기 작업 종료 후 실행되는 completion handler 가 가장 좋은 예시.
How? : 파라미터 클로저의 타입 앞에 @escaping 작성
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) }
Swift
복사
someFunctionWithEscapingClosure(_:) 함수는 클로져를 인자로 받은 뒤 함수 외부에 선언된 어레이에 이를 추가합니다.
함수가 종료 된 후에 클로저가 실행되어야 하므로 이를 @escaping 을 사용해서 타입선언을 해준 모습입니다.
이렇게 선언을 해주어야 함수 외부에서 클로저가 호출될 수 있습니다.
외부에서 접근,실행 하는 escaping closure 를 @escaping 특성을 사용하지 않으면 컴파일 에러가 발생합니다!
모든 클래스의 객체는 self 라는 암묵적 속성을 가집니다. 이는 인스턴스 자신과 정확하게 동일합니다.
이 속성을 활용해서 인스턴스 메소드 내에서 현재 인스턴스를 참조할 수 있습니다.
인스턴스 메소드의 파라미터가 인스턴스 프로퍼티와 이름이 같을 때, 구분해주기 위하여 인스턴스 프로퍼티에는 self.xx을 사용합니다.
일반적으로 클로져는 클로져 body 내에서 변수를 사용하면 이를 암묵적으로 캡쳐합니다. self 를 참조하는 Escaping 클로저는 신중하게 사용해야 합니다. Escape 클로저에서 self 를 캡쳐하면 실수로 strong 참조를 하게 됩니다.
이는 Swift 의 ARC 로 인해 메모리 낭비가 발생할 수 있으니 조심해야합니다.
var completionHandlers: [() -> Void] = [] // escaping 클로저 func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) } // non escaping 클로저 func someFunctionWithNonescapingClosure(closure: () -> Void) { closure() // 함수 내부에서만 실행됨. } class SomeClass { var x = 10 func doSomething() { // 명시적으로 self 작성 // self 를 참조하기 때문에. 지속적인 원본 변경 가능성 및 메모리 낭비 someFunctionWithEscapingClosure { self.x = 100 } // 암묵적으로 self 참조 이는 escape 클로저가 아니기 때문. // 함수가 종료됨에 따라 클로저의 메모리 공간도 반환되므로 문제 발생 X someFunctionWithNonescapingClosure { x = 200 } } } let instance = SomeClass() instance.doSomething() print(instance.x) // Prints "200" completionHandlers.first?() print(instance.x) // Prints "100"
Swift
복사
왜 non escaping 클로저 escaping 클로저를 구분 지을까?
컴파일러의 퍼포먼스와 최적화 때문입니다.
non-escaping 클로저 : 컴파일러가 클로저의 실행이 언제 종료되는지 알기 때문에, 때에 따라 클로저에서 사용하는 특정 객체에 대한 retain, release 등의 처리를 생략해 객체의 라이프싸이클(life-cycle)을 효율적으로 관리할 수 있습니다.
esacping 클로저 : 함수 밖에서 실행되기 때문에 클로저가 함수 밖에서도 적절히 실행되는 것을 보장하기 위해, 클로저에서 사용하는 객체에 대한 추가적인 참조싸이클(reference cycles) 관리 등을 해줘야 합니다. 이 부분이 컴파일러의 퍼포먼스와 최적화에 영향을 끼치기 때문에 Swift에서는 필요할 때만 escaping 클로저를 사용하도록 구분해 두었습니다
클로저의 캡쳐리스트에 self 를 캡쳐해 둔후 self 를 암묵적으로 참조하는 모습입니다.
class SomeOtherClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { [self] in x = 100 } someFunctionWithNonescapingClosure { x = 200 } } }
Swift
복사
만약 self 가 structure 또는 enumeration 의 객체라면, 항상 self 를 암묵적으로 참조할 수 있습니다.
하지만 escaping closure 는 self 가 변경 가능한 객체일 때는 이를 캡쳐할 수 없습니다.
Structure 와 Enumeration 은 공유를 통한 변경을 허용하지 않습니다. 자세한 사항은 Structures and Enumerations Are Value Types 문서에서 확인하세요
struct SomeStruct { var x = 10 mutating func doSomething() { someFunctionWithNonescapingClosure { x = 200 } // Ok someFunctionWithEscapingClosure { x = 100 } // Error } }
Swift
복사
위와 같이 struct 를 escaping closure 가 참조하려 하면 컴파일 에러를 발생합니다.
mutating 키워드를 통해 함수가 선언되어 self 가 mutable 해지기 때문인데, 이는 escaping closure 가 변경가능한 객체를 캡쳐할 수 없다는 규칙을 위반합니다.

Autoclosures

@autoclosure
when? : 일반 표현의 코드를 클로저 표현으로 만들어주고자 할 때
how? : 인자가 없고 리턴값만 존재하는 클로저를 사용해야함.
아래는 일반 표현의 코드
func normalPrint(_ closure: () -> Void) { closure() } normalPrint({ print("I'm Normal Expression") })
Swift
복사
아래가 @autoclosure 를 사용한 경우
func autoClosurePrint(_ closure: @autoclosure () -> Void) { closure() } autoClosurePrint(print("I'm AutoClosure Expression"))
Swift
복사
차이점이 느껴지시나요?
바로 {} 를 생략하고 바로 표현식을 함수의 인자로서 전달해 줄 수있다는 점이죠.
훨씬 코드를 간결하게 만들 수 있습니다.
이를 활용하는 대표적인 예로 assert() 함수가 있습니다.
public func assert(_ condition: @autoclosure () -> Bool, _ message: @autoclosure () -> String = String())
Swift
복사
다음과 같이 호출합니다
assert(false, "Error Occurred!") assert(0 < 1, "Error Occurred!") assert(false, (statusCode == .fileNotFound) ? "File Not Found!" : "Something going wrong!")
Swift
복사
이때 @autoclsoure 를 제거하고
func assert(_ condition: () -> Bool, _ message: String = String()) { }
Swift
복사
위 처럼 똑같이 함수를 호출해보면
assert({ false }, { "Error Occurred!" }) assert({ 0 < 1 }, { "Error Occurred!" }) assert({ false }, {(statusCode == .fileNotFound) ? "File Not Found!" : "Something going wrong!"})
Swift
복사
코드가 더 복잡해 진것을 볼 수 있습니다.
즉 한마디로 클로저 전달시 {} 를 생략할 수 있게 해줍니다.
추가로 escape 와 autoclosure 를 함께 사용할 수도 있습니다.
// customersInLine is ["Barry", "Daniella"] var customerProviders: [() -> String] = [] func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) { customerProviders.append(customerProvider) } collectCustomerProviders(customersInLine.remove(at: 0)) collectCustomerProviders(customersInLine.remove(at: 0)) print("Collected \(customerProviders.count) closures.") // Prints "Collected 2 closures." for customerProvider in customerProviders { print("Now serving \(customerProvider())!") } // Prints "Now serving Barry!" // Prints "Now serving Daniella!"
Swift
복사
연달아서 @autoclosure @escaping 특성을 부여해주면 됩니다.