[Swift] 공식문서 07 - Closures (클로저)
Closures — The Swift Programming Language (Swift 5.5)
Closures Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. Closures can capture and store referen
docs.swift.org
1차 수정 (2022 / 02 / 26) : 의역 및 오타, 스타일 수정
Closures (클로저)
클로저는 코드에서 기능을 가진 블럭(block) 입니다. Swift의 클로저는 C언어와 Objective-C의 블럭과 다른 프로그래밍 언어들의 람다(lambda) 와 비슷합니다.
클로저는 정의된 상수와 변수의 레퍼런스에 대해 캡쳐(capture)하고 저장합니다. 이것은 상수와 변수에 대한 closing-over로 알려져 있습니다. Swift는 사용자를 위해 capturing의 모든 메모리 관리를 다룹니다.
capturing은 이 장의 뒤에서 자세히 다룹니다.
Functions 챕터에서 다루었던 전역함수, 중첩함수는 사실 클로져의 특별한 케이스 입니다. 클로져는 3가지 형태 중 하나를 가집니다.
- 전역(global) 함수는 이름을 가지고 있지만 어떤 값도 캡쳐하지 못하는 클로저이다.
- 중첩함수는 이름을 가지고 그들을 감싸하고 있는 함수의 값을 캡쳐할 수 있다.
- 클로져 표현식은 짧은 구문으로 쓰여진 이름이 없는 클로져이며, 주변 코드(구문)의 값들을 캡쳐할 수 있다.
Swift의 클로져 표현식은 일반적인 경우에 짧고 혼란스럽지 않은 구문을 유도하는 최적화와 함께, 깔끔하고 명확한 스타일을 가지고 있습니다.
- 구문으로부터 매개변수와 리턴 값 타입을 추론한다.
- 클로저 단일 표현식에서 암시적 리턴을 한다
- 인수 이름을 짧게 선언한다.
- 클로저 구문이 따라온다.
Closure Expression(클로저 표현식)
중첩함수는 코드가 포함하는 블록의 이름을 지정하고 함수를 더 크게 정의 할 수 있는 편리한 방법입니다. 그러나 때로는 전체 선언과 이름이 없는, 함수형 구조의 더 짧은 버전을 쓰는게 유용합니다. 특히 함수를 하나 이상의 인수로 사용하는 함수 또는 메서드를 사용할 때 그렇습니다. 클로저 표현식은 짥고 요약된 구문을 인라인 클로저를 작성하기 위한 방식입니다. 클로저 표현식은 명확성과 의도의 손실 없이 짧은 클로져 작성을 위해 여러 구문 최적화를 제공합니다.
The Sorted Method (정렬 방법)
Swift의 기본 라이브러리는 sorted(:by) 라고 불리는 메소드를 제공합니다. 이 메소드는 사용자가 제공하는 정렬 클로저의 출력에 기반해 배열 타입으로 반환해 줍니다. 정렬이 끝나면 sorted 메소드는 입력의 타입과 크기가 같은, 정렬된 새로운 배열을 리턴합니다.
let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
sorted 메소드는 배열의 두 개의 타입을 비교해 Bool 값을 리턴하는 클로져를 인수로 받습니다. 클로져는 첫 번째 인수가 크면 true, 두 번째 인수가 크면 false를 리턴합니다. s1 이 s2 보다 커서 true를 리턴하면 새로운 배열 에서는 s1이 s2 보다 앞에 온다는 것을 의미합니다.
Closure Expression Syntax(클로져 표현 구문)
클로져 표현 구문은 다음과 같은 형식을 따릅니다
{ (parameters) -> return-type in
statements
}
클로져에 있는 매개변수(parameter)는 in-out 매개변수가 될 수 있지만, 기본 값(default value)을 가지지는 못합니다. variadic 매개변수를 이름으로 지으면 Variadic 매개변수가 사용 될 수 있습니다. 튜플 또한 매개변수 타입과 리턴 타입으로 쓰일 수 있습니다.
정렬 예제를 클로져 표현식으로 작성하면 아래와 같습니다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
위 예제의 인라인 클로져의 매개변수와 리턴 타입 선언은 backward 함수와 동일하다는 것을 주목하십시오. 두 개의 경우 모두 (s1: String, s2: String) -> Bool 입니다. 그렇지만 이 인라인 클로져 표현식에서는 매개변수와 리턴타입은 { } 안에 작성되었습니다.
인라인이란 코드가 안에 있다는 것. 인라인 클로저는 sorted(by:) 메소드 안에 있기 때문에 인라인이다.
클로져의 시작은 in 키워드에 의해 시작합니다. 이 키워드는 클로져의 매개변수와 리턴 타입의 정의가 끝났으며, 클로져의 본문이 시작한다는 것을 말해줍니다.
1줄로도 작성할 수 있습니다.
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )
Inferring Type From Context (구문에서의 타입 추론)
Sorting 클로져가 메소드의 인수로 들어가므로, Swift는 그 파라미터와 리턴하는 값의 타입을 추론할 수 있습니다. sorted(by:) 메소드는 문자열 배열에서 호출되고 있으므로, 그 인수는 (String, String) -> Bool 타입의 함수이여야 합니다.
즉 (String, String) -> Bool 는 클로져 표현식 정의에서 생략될 수 있습니다.
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
클로져를 함수 또는 메소드에 인라인 클로져 표현식으로 전달 하면, 매개변수 타입과 리턴 타입을 유추하는게 항상 가능합니다. 결과적으로 인라인 클로져를 작성할 때 생략할 수 있습니다.
생략할 수도 있지만, 원하는 경우 타입을 명시할 수 있으며, 모호함을 피해 가독성을 증가시키기 위해서라면 생략 안 하는 것이 권장됩니다.
Implicit Returns from Single-Expression Closures(단일 표현 클로저의 암시적 리턴)
단일 표현 클로져는 return 키워드를 생략하고 암시적으로 리턴할 수 있습니다.
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
closure 에서 명백히 Bool 값을 리턴하는 s1 >s 2 표현식을 포함하고 있고 그에 따라 return 키워드가 생략될 수 있습니다.
Shorthand Argument Names(인수 이름 단축)
Swift는 자동으로 인라인 클로저에 인수 이름 단축을 제공합니다. $0, $1, $2 처럼 작성될 수 있습니다.
이에 따라 클로저 정의에서 인수를 생략할 수 있습니다. in 키워드 또한 생략될 수 있습니다.
reversedNames = names.sorted(by: { $0 > $1 } )
Operator Mehods (연산자 방법)
클로져 표현식을 통틀어 가장 짧게 쓸 수 있는 방법이 있습니다. Swift는 > 를 메소드로 사용해 두 개의 String 매개변수를 비교하고 Bool 타입을 반환합니다.
reversedNames = names.sorted(by: >)
Trailing Closures(뒤따라오는 클로져)
만약 함수의 마지막 인수로 클로져 표현식을 넣어줘야하는데 너무 길다면, trailing closure를 사용하는 게 유용할 수 있습니다. trailing closure가 함수의 인수일지라도 클로져를 함수 호출 } 뒤에 적습니다. trailing closure 를 사용하면, 함수 호출의 부분으로 첫번쨰 클로져 인수 이름을 작성하지 않아도 됩니다. (실제로는 단일 trailing closure를 잘 안쓴다)
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// Here's how you call this function without using a trailing closure:
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// Here's how you call this function with a trailing closure instead:
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
위의 예제에서 함수의 () 안이 아닌 {} 안에 클로져를 작성해서 trailing closure를 사용했습니다.
reversedNames = names.sorted() { $0 > $1 }
위의 예제를 trailing closure로 작성하면 ()를 사용할 필요 없이 { } 안에 표현식을 작성합니다.
reversedNames = names.sorted { $0 > $1 }
trailing closure 은 인라인 클로져가 너무 길어 한 줄에 적을 수 없을 때 유용합니다.
let digitNames = [
0: "Zero", 1: "One", 2: "Two", 3: "Three", 4: "Four",
5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]
원래대로라면 map(_:) 메소드 내부에 클로져를 적어주어야 합니다. 하지만 trailing closure 를 사용해 {} 에 적어줍니다.
let strings = numbers.map { (number) -> String in
var number = number
var output = ""
repeat {
output = digitNames[number % 10]! + output
number /= 10
} while number > 0
return output
}
// strings is inferred to be of type [String]
// its value is ["OneSix", "FiveEight", "FiveOneZero"]
map(_:) 메소드는 배열의 아이템 하나 하나에 클로져 표현식을 호출합니다. 배열이 매핑되면서 타입이 추론되므로, 클로져의 입력값의 타입을 생략해도 됩니다.
만약 함수가 여러개의 클로져를 받으면, 첫 번째 trailing closure의 인수 이름을 생략하고 나머지 trailing closure에 인수를 작성합니다.
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
위의 예제에서는 함수는 server 인수 하나와 두 개의 클로져를 인수로 받습니다.
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
첫 번째 클로저인 completion의 표현식은 trailing closure로 작성되어, 인수 이름이 생략된채로 { } 안 에 작성되었습니다. 두 번째 클로저는 인수 이름 onFailure: 를 {} 앞에 작성해주었습니다.
Capturing Values (값 캡처링)
클로져는 변수와 정수가 정의된 곳을 둘러싼 구문에서, 그 둘을 캡처(capture) 할 수 있습니다. 클로저는 상수와 변수를 정의했던 원래 범위를 벗어나더라도, 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다.
Swift에서, 값을 캡처할 수 있는 가장 간단한 형식의 클로져는, 다른 함수의 실행문에 들어있는 중첩 함수입니다. 중첩함수는 감싸고 있는 함수의 인수, 변수 상수를 캡처 할 수 있습니다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
중첩 함수 incrementer()를 감싸고 있는 함수 makeIncrementer에서 선언된 amount, runningTotal 변수를 캡처합니다.
incrementer() 함수만 따로 떼어놓고 보면 일반적이지 않습니다.
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
어떤 매개변수도 가지고 있지 않은데 amount와 runningTotal에 접근합니다. 이는 둘러싼 함수의 변수인 amount와 runningTotal의 참조(reference)를 캡처링하기 때문입니다. 참조에 의한 캡처링은 makeIncrementer 함수가 끝나도 amount와 runningTotal가 사라지지 않는 것을 보증합니다. 또한 runningTotal은 다음 번 incrementer 함수가 불릴 때 유효합니다.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30
참조에 의한 캡쳐링 때문에 incrementByTen 값이 끝났음에도 그 값이 저장되어, 다음 번 호출 시 누적되는 것을 확인할 수 있습니다.
Closures Are Reference Types(클로져는 참조 타입이다)
위의 예제에서 incrementByTen은 상수임에도 불구하고 runningTotal 변수는 계속해서 증가했습니다. 이러한 이유는 함수와 클로져가 참조 타입(reference types) 이기 때문입니다.
함수와 클로져를 상수나 변수에 할당할 때, 사실은 상수랑 변수를 함수와 클로져의 참조타입으로 설정하는 것입니다.
두 개의 다른 변수나 상수에 클로져를 할당했을 때, 두 개 모두 같은 클로져를 참조한다는 것 입니다.
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60
새로운 상수 alsoIncrementByTen을 선언해 실행했는데 기존의 값에 누적해서 50이 나옵니다. 다시 기존의 상수incrementByTen 를 실행했는데 이 또한 누적되서 60을 리턴합니다.
두 개의 상수가 똑같은 클로저를 참조하고 있기 때문에 이와 같은 결과가 나옵니다.
Escaping Closures(이스케이핑 클로져)
클로져는 클로져가 함수의 인자로 통과될 때 함수를 탈출한다고 말합니다. 하지만 함수 리턴 이후에 호출됩니다. 함수에서 클로져를 매개변수로 쓸 때, 그 앞에 @escaping 를 작성해 클로져가 탈출할 수 있도록 명시합니다.
클로져가 탈출하는 하나의 방법은 함수 밖에 선언된 변수에 저장되는 것입니다. 예로, 비동기 연산으로 시작하는 많은 함수에서 completion handler로써 클로저를 인수로 사용합니다. 함수는 비동기 연산이 시작한 후에 리턴하지만, 클로져는 연산이 끝날 때 까지 호출되지 않습니다. 클로져는 탈출해서 나중에 호출되어야 합니다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
위의 예제에서 함수는 클로져를 인수로 사용해서 함수 밖에 선언된 배열에 추가합니다. 만약 @escaping을 사용하지 않으면 컴파일 타임 에러가 발생합니다.
self를 참조하는 이스케이핑 클로져는 사용할 때 특별히 신중해야합니다. 이스케이핑 클로져에서 self를 캡처링하는 것은 강력한 참조 사이클 (strong refernence cycle)을 쉽게 만들 수 있습니다.
일반적으로 클로져는 클로져 실행문에서 변수를 사용함으로써 암시적으로 캡쳐를 하지만, 이 경우에는 명확히 해야합니다. 만약 self를 캡쳐하고 싶으면, self를 쓸 때 명확히 작성해야하며, 혹은 self를 클로져의 캡쳐리스트에 포함합니다. self를 쓰는 것은 사용자의 의도를 분명하게 표현하도록 해주고, 사용자에게 참조 주기가 없다는 것을 확인시켜줍니다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
func someFunctionWithNonescapingClosure(closure: () -> Void) {
closure()
}
class SomeClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { self.x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"
completionHandlers.first?()
print(instance.x)
// Prints "100"
someFunctionWithEscapingClosure 함수는 escaping을 사용했으므로 self를 명시적으로 참조해야 합니다. someFunctionWithNonescapingClosure 함수는 escaping을 사용하지 않아 self를 암시적으로 참조할 수 있습니다. 즉 생략이 가능합니다.
아래 예제는 클로져의 캡쳐 리스트에 self를 포함한 doSometing함수입니다.
class SomeOtherClass {
var x = 10
func doSomething() {
someFunctionWithEscapingClosure { [self] in x = 100 }
someFunctionWithNonescapingClosure { x = 200 }
}
}
self가 구조체이거나 열거형이면 인스턴스는 항상 self를 참조 할 수 있습니다. 하지만 self 가 구조체 또는 열거형의 인스턴스인 경우 이스케이프 클로저는 self 에 대한 중복된 참조를 캡쳐할 수 없습니다.
struct SomeStruct {
var x = 10
mutating func doSomething() {
someFunctionWithNonescapingClosure { x = 200 } // Ok
someFunctionWithEscapingClosure { x = 100 } // Error
}
}
위의 예제에서 self 가 중복되므로 에러가 발생합니다.
Autoclosures(오토클로져)
오토클로져는 함수에 인수로 전달되는 표현식을 감싸기 위해 자동으로 생성되는 클로져입니다. 어떤 인수도 필요로 하지 않으며, 호출할 때 감싸고 있는 표현식의 값을 리턴합니다. 이러한 편리성을 통해 명시적 클로져 대신 정규 표현식을 작성하여 함수 주변의 {} 를 생략할 수 있습니다.
보통 오토클로져를 같는 함수를 `호출` 한다고 하지, `실행`한다고 하지는 않습니다. 예를 들어 assert(condition:message:file:line:) 함수는 condition, message 매개변수를 위해 오토 클로저를 사용합니다. condition 매개변수는 디버그 빌드에서만 실행되며, message 매개변수는 condition 이 false 일때만 실행됩니다.
오토클로져를 사용하면 유저가 클로져를 호출할 때까지 안에 있는 코드가 실행되지 않기 때문에 실행을 지연시킵니다. 실행을 지연시키는것은 코드의 실행 시기를 제어할 수 있기 때문에, 부작용이 있거나 계산이 많이 필요한 코드에 유용합니다. 아래 예제는 클로져가 실행을 지연시키는 방법을 보여줍니다.
var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"
let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"
print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"
배열의 첫번째 항목을 클로져안의 표현식을 통해 제거하지만, 배열은 클로져가 실제로 호출될 때까지 제거되지 않습니다. 클로져가 호출되지 않으면 클로져 안의 표현식은 영원히 실행되지 않습니다.
함수의 인수로 클로져를 통과시킬 때 실행을 지연시키는 똑같은 동작을 볼 수 있습니다.
// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"
serve 함수는 cutomer 이름을 리턴하는 명시적인 클로져를 포함합니다. 클로져를 사용하기전에 @autoclosure를 작성하여 오토클로져를 사용합니다. 이렇게 작성하면 함수는 클로져 대신 string을 인수로 가집니다. @autoclosure 속성으로 인해 인수는 자동으로 클로져로 변환이 됩니다.
// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"
오토클로져의 남용은 코드의 가독성을 저하시킵니다. 구문과 함수이름을 통해 실행이 지연되고 있음을 명확히 해야합니다.
오토클로져를 탈출(escape)시키고 싶으면 @autoclosure 와 @escaping을 동시에 사용합니다.
// 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!"
예제에서 customProviders를 인수로 클로져를 호출하는 대신, collectcustomProviders 함수는 클로져를 customerProviders 배열에 추가합니다. 배열은 함수 밖에 선언되는데, 함수가 리턴한 후에 실행이 됩니다. 결과적으로 customProviders 인수 값이 함수의 범위를 벗어나도록 @escaping을 사용합니다.
클로져, 흔히들 익명함수라고 하는데요.
왜 이름이 없는 함수인지 정리하면서 공부하다보니 조금은 이해가 됩니다.
제가 들었던 강의에서는 클로져를 작성할 때 과도하게 줄여쓰는 것을 추천하지 않았습니다.
같이 일하는 팀원들이 읽기가 힘들고, 작성자 나 자신도 오랜만에 읽으면 뭔지 헷갈릴 때가 있다고 합니다.
가독성을 유지한 상태에서 적절히 클로져를 줄여쓰는 노력을 해야겠습니다.