클로저(Closure)는 일정 기능을 하는 코드를 하나의 블록으로 모아놓은 것을 말한다.
변수나 상수가 선언된 위치에서 참조(Reference)를 획득(Capture)하고 저장할 수 있다.
획득(Capture) 때문에 메모리에 부담이 가지 않을까 걱정할 수도 있지만, swift는 스스로 메모리를 관리한다.
클로저의 몇 가지 모양 중 하나가 함수이다. 클로저의 세 가지 형태는 다음과 같다.
swift에서 클로저 표현은 최적화되어서 간결하고 명확하다. 최적화의 의미는 다음과 같다.
클로저 표현은 인라인 클로저를 명확하게 표현하는 방법으로 문법에 초점이 맞춰져 있다.
let names = ["bronzeCastle", "silverCastle", "goldCastle"]
예를 들어, 위와 같이 문자열을 원소로 가지는 배열이 있다고 가정하자. 이 배열을 정렬하기 위해서 swift의 표준 라이브러리에 있는 sorted(by:)라는 메소드를 사용하면 된다. by에 어떤 방법으로 정렬을 수행할 것인지에 대해 기술한 클로저를 넣으면 되는데 예시를 보면서 이해해보자.
func backward(_ s1: String, _ s2: String) -> Bool {
return s1 > s2
}
var reversedNames = names.sorted(by: backward)
결과
["silverCastle", "goldCastle", "bronzeCastle"]
backward 클로저를 만들어 원본 배열을 내림차순으로 정렬한 것을 알 수 있다. 비교하는 클로저를 사용하는데 긴 코드를 사용하였는데 앞으로 클로저의 다양한 문법을 사용하면서 익숙해보자.
클로저 표현 문법은 일반적으로 아래의 형태를 띤다.
{ (parameters) -> return type in
statements
}
parameters
는 인자로 넣을 곳, statements
는 인자 값으로 처리할 내용을 기술할 곳이라고 생각하면 된다.
앞에서 사용했던 backward 클로저를 다음과 같은 클로저 표현을 이용해 바꿀 수 있다.
var reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
sorted(by:)의 메소드에서 이미 (String, String) -> Bool
타입이 인자로 들어올 것을 알기 때문에 타입을 추론하여 생략할 수 있다.
var reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 })
단일 표현 클로저에서는 return
키워드를 생략할 수 있다.
var reversedNames = names.sorted(by: { s1, s2 in s1 > s2 })
swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공한다. 인자 값 순서대로 $0, $1, $2...으로 사용할 수 있다.
축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력받는 부분과 in
키워드를 생략할 수 있다.
var reversedNames = names.sorted(by: { $0 > $1 })
만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있다. 이런 형태의 함수와 클로저가 있다면,
func fun(closure: () -> Void) {
// function body goes here
}
위 클로저의 인자 값 입력 부분과 반환 형 부분을 생략해 다음과 같이 표현할 수 있고
fun(closure: {
// closure's body goes here
})
이것을 후위 클로저로 표현하면 아래와 같이 표현할 수 있다.
이런 형태를 우린 자주 사용했는데 사실 이런 일반적인 전역함수 형태가 클로저를 사용하고 있던 것이었다.
fun() {
// trailing closure's body goes here
}
클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 캡쳐라는 의미가 생소할 수 있는데 말 그대로 원본 값이 사라져도 그 값을 포획을 하여 클로저의 body 안에서 활용할 수 있다. swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수(nested function)이다.
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
클로저의 인자 값은 (forIncrement amount: Int)이고, 반환 값은 () -> Int이다.
즉, 반환 값을 인자가 없고 Int형의 클로저를 반환한다는 의미이다. 이 중첩 함수를 실행해보겠다.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementByTen()
incrementByTen()
결과
10
20
30
함수가 각각 실행되지만 실제로는 변수 runningTotal과 amount가 캡쳐링되어서 그 변수를 공유하기 때문에 계산이 누적됨을 알 수 있다.
여기서 무언가 이상함을 눈치챘을 수도 있다.
incrementByTen은 var가 아닌 let으로 선언되었기 때문에 상수인데 어떻게 runningTotal 변수를 계속해서 증가시킬 수 있었을까?
바로, 함수와 클로저는 참조 타입이기 때문이다. 함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference)가 할당됨을 잊지 말자.