클로저

클로저

이름 없는 함수를 만들지 않고도, 함께 실행되는 코드를 하나로 묶어 쓰는 방법이에요. 클로저는 전달하고 재사용할 수 있는 독립된 기능 블록인데요, 다른 언어에서 말하는 클로저, 익명 함수, 람다, 블록과 비슷한 개념이에요.

클로저는 정의된 주변 맥락에 있는 상수와 변수의 **참조를 포착(capture)**해서 저장할 수 있어요. 이걸 두고 그 상수와 변수를 *닫아 둔다(closing over)*고 해요. 포착에 필요한 메모리 관리는 Swift가 전부 알아서 처리해요.

참고: 포착(capturing)이라는 개념이 처음이라면 걱정하지 마세요. 아래 '값 포착하기' 절에서 자세히 다룰게요.

전역 함수와 중첩 함수, 즉 doc:Functions에서 소개한 함수들은 사실 클로저의 특수한 형태예요. 클로저는 다음 세 가지 모습으로 나타나요:

  • 전역 함수는 이름이 있고 어떤 값도 포착하지 않는 클로저예요.
  • 중첩 함수는 이름이 있고, 자기 바깥 함수의 값들을 포착할 수 있는 클로저예요.
  • 클로저 표현식은 이름이 없는 클로저로, 가벼운 문법으로 쓰고 주변 문맥의 값을 포착할 수 있어요.

Swift의 클로저 표현식은 깔끔하고 명확한 스타일을 갖추고 있죠. 흔한 상황에서는 간결하게 쓰도록 돕는 최적화도 함께 제공해요. 그 최적화 항목은 이래요:

  • 문맥에서 매개변수 타입과 반환 타입 추론
  • 단일 표현식 클로저의 암시적 반환
  • 축약 인자 이름(Shorthand Argument Names)
  • 후행 클로저(Trailing Closure) 문법

출처: The Swift Programming Language

본문

클로저 표현식

doc:Functions#Nested-Functions에서 소개한 중첩 함수는 더 큰 함수의 일부로 쓰이는 독립된 코드 블록을 이름 붙여 정의하기 편리한 방법이에요. 그런데 때로는 전체 선언과 이름 없이, 함수 같은 구조를 더 짧게 쓰고 싶을 때가 있어요. 특히 함수 또는 메서드가 함수를 인자로 하나 이상 받을 때 그런 경우가 많죠.

클로저 표현식은 인라인 클로저를 간결하고 집중된 문법으로 쓰는 방법이에요. 클로저 표현식은 명확성이나 의도를 잃지 않으면서 클로저를 짧은 형태로 쓸 수 있게 여러 문법 최적화를 제공해요. 아래 클로저 표현식 예시들은 sorted(by:) 메서드 하나를 여러 번 다듬어 가면서 각 최적화를 보여줘요. 매번 같은 기능을 더 간결하게 표현하죠.

sorted(by:) 메서드

Swift 표준 라이브러리는 sorted(by:)라는 메서드를 제공해요. 이 메서드는 여러분이 넘겨주는 정렬 클로저의 출력에 따라, 알려진 타입의 값 배열을 정렬하죠. 정렬이 끝나면 sorted(by:) 메서드는 이전 배열과 같은 타입·같은 크기의 새 배열을 돌려줘요. 요소는 올바른 정렬 순서로 되어 있죠. 원래 배열은 sorted(by:) 메서드가 건드리지 않아요.

아래 클로저 표현식 예시들은 sorted(by:) 메서드로 String 값 배열을 알파벳 역순으로 정렬해요. 정렬할 초기 배열은 이렇게 생겼어요.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

sorted(by:) 메서드는 두 개의 인자를 받는 클로저를 받아요. 두 인자는 배열 내용물과 같은 타입이어야 하고, 정렬 후 첫 번째 값이 두 번째 값보다 앞에 와야 하는지를 Bool 값으로 돌려주죠. 정렬 클로저는 첫 번째 값이 두 번째 값보다 에 와야 하면 true를, 그 외에는 false를 돌려주면 돼요.

이 예시는 String 값 배열을 정렬하니까, 정렬 클로저는 (String, String) -> Bool 타입의 함수여야 해요.

정렬 클로저를 제공하는 한 가지 방법은 올바른 타입의 일반 함수를 작성해서 sorted(by:) 메서드에 인자로 넘기는 거예요.

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"]

첫 번째 문자열(s1)이 두 번째 문자열(s2)보다 크면, backward(_:_:) 함수는 true를 돌려줘요. 이는 정렬된 배열에서 s1s2보다 앞에 와야 한다는 뜻이죠. 문자열의 문자에서 "크다"는 "알파벳에서 더 뒤에 온다"를 의미해요. 즉 "B""A"보다 크고, "Tom""Tim"보다 큰 문자열이에요. 그래서 알파벳 역순 정렬이 되어 "Barry""Alex"보다 앞에 놓이게 되는 거죠.

하지만 이렇게 쓰면 본질적으로 단일 표현식 함수(a > b)인 것을 너무 장황하게 적는 셈이에요. 이 예시에서는 정렬 클로저를 클로저 표현식 문법으로 인라인으로 쓰는 편이 더 좋겠죠.

클로저 표현식 문법

클로저 표현식 문법은 다음과 같은 일반적인 형태를 가져요.

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

클로저 표현식 문법의 매개변수는 in-out 매개변수가 될 수 있지만 기본값은 가질 수 없어요. 가변 매개변수는 이름을 붙이면 쓸 수 있고, 튜플도 매개변수 타입과 반환 타입으로 쓸 수 있죠.

아래 예시는 위의 backward(_:_:) 함수를 클로저 표현식으로 쓴 버전이에요.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

이 인라인 클로저의 매개변수와 반환 타입 선언이 backward(_:_:) 함수에서의 선언과 동일하다는 걸 눈여겨보세요. 두 경우 모두 (s1: String, s2: String) -> Bool로 쓰이죠. 다만 인라인 클로저 표현식에서는 매개변수와 반환 타입이 중괄호 안쪽에 쓰여요. 바깥이 아니라요.

클로저 본문의 시작은 in 키워드로 알 수 있어요. 이 키워드는 클로저의 매개변수와 반환 타입 정의가 끝났고, 본문이 시작된다는 것을 나타내죠.

클로저 본문이 워낙 짧아서 한 줄로도 쓸 수 있어요.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in return s1 > s2 } )

이를 통해 sorted(by:) 메서드 호출 자체는 그대로라는 걸 알 수 있어요. 메서드의 전체 인자를 감싸는 괄호 한 쌍은 여전히 존재하죠. 다만 그 인자가 이제 인라인 클로저가 된 거예요.

문맥에서 타입 추론하기

정렬 클로저가 메서드의 인자로 전달되기 때문에, Swift는 클로저 매개변수의 타입과 반환 값의 타입을 추론할 수 있어요. sorted(by:) 메서드가 문자열 배열에서 호출되고 있으니, 그 인자는 (String, String) -> Bool 타입의 함수여야 하죠. 즉 (String, String)Bool 타입을 클로저 표현식 정의의 일부로 쓸 필요가 없어요. 모든 타입이 추론 가능하니, 반환 화살표(->)와 매개변수 이름을 감싼 괄호도 생략할 수 있죠.

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

클로저를 함수나 메서드에 인라인 클로저 표현식으로 전달할 때는 항상 매개변수 타입과 반환 타입을 추론할 수 있어요. 그 결과, 클로저를 함수나 메서드 인자로 쓸 때는 인라인 클로저를 가장 긴 형태로 쓸 필요가 절대 없어요.

그래도 원하면 타입을 명시할 수 있어요. 코드를 읽는 사람에게 모호함을 피해 주는 경우라면 오히려 명시하는 게 권장되죠. sorted(by:) 메서드의 경우에는 정렬이 일어난다는 사실에서 클로저의 목적이 분명하고, 문자열 배열의 정렬을 돕고 있으니 클로저가 String 값을 다룰 것이라고 독자가 안전하게 짐작할 수 있어요.

단일 표현식 클로저의 암시적 반환

단일 표현식 클로저는 선언에서 return 키워드를 빼면, 그 단일 표현식의 결과를 암시적으로 반환할 수 있어요. 앞 예시를 이렇게 써 보죠.

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

여기서는 sorted(by:) 메서드 인자의 함수 타입 덕분에 클로저가 Bool 값을 돌려줘야 한다는 게 분명해요. 클로저 본문이 Bool 값을 반환하는 단일 표현식(s1 > s2)만 담고 있으니 모호함이 없고, return 키워드를 생략할 수 있죠.

축약 인자 이름

Swift는 인라인 클로저에 축약 인자 이름을 자동으로 제공해요. $0, $1, $2처럼 써서 클로저 인자의 값들을 가리킬 수 있죠.

클로저 표현식 안에서 이 축약 인자 이름을 쓰면, 정의에서 클로저의 인자 목록을 생략할 수 있어요. 축약 인자 이름의 타입은 기대되는 함수 타입에서 추론되고, 여러분이 쓰는 가장 큰 번호의 축약 인자가 클로저가 받는 인자 수를 결정해요. 클로저 표현식이 전부 본문으로만 이루어져 있으니 in 키워드도 생략할 수 있죠.

reversedNames = names.sorted(by: { $0 > $1 } )

여기서 $0$1은 클로저의 첫 번째와 두 번째 String 인자를 가리켜요. $1이 가장 큰 번호의 축약 인자이므로 클로저는 두 인자를 받는 것으로 이해돼요. sorted(by:) 함수가 두 인자가 모두 문자열인 클로저를 기대하므로, 축약 인자 $0$1은 둘 다 String 타입이 돼요.

연산자 메서드

사실 위 클로저 표현식보다도 짧게 쓸 방법이 있어요. Swift의 String 타입은 크다(>) 연산자를 문자열 전용으로 구현한 메서드를 두 개의 String 타입 매개변수와 Bool 타입 반환 값으로 정의해놨어요. 이는 sorted(by:) 메서드가 요구하는 메서드 타입과 정확히 일치하죠. 그래서 크다 연산자를 그대로 넘기면, Swift가 문자열 전용 구현을 쓰고 싶다는 걸 추론해요.

reversedNames = names.sorted(by: >)

연산자 메서드에 대한 자세한 내용은 doc:AdvancedOperators#Operator-Methods에서 확인하세요.

후행 클로저

클로저 표현식을 함수의 마지막 인자로 전달해야 하는데 그 클로저 표현식이 길다면, 후행 클로저로 쓰는 게 유용할 때가 있어요. 후행 클로저는 함수 호출 괄호 뒤에 써요. 여전히 함수의 인자이긴 하지만요. 후행 클로저 문법을 쓰면 첫 번째 클로저의 인자 라벨을 함수 호출의 일부로 쓰지 않아요. 함수 호출이 여러 후행 클로저를 가질 수도 있고요. 다만 아래 첫 몇 예시는 단일 후행 클로저를 써요.

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
}

위에서 살펴본 doc:Closures#Closure-Expression-Syntax의 문자열 정렬 클로저는 sorted(by:) 메서드 괄호 바깥에 후행 클로저로 쓸 수 있어요.

reversedNames = names.sorted() { $0 > $1 }

클로저 표현식을 함수나 메서드의 유일한 인자로 제공할 때, 그 표현식을 후행 클로저로 제공한다면 함수를 호출할 때 함수나 메서드 이름 뒤에 괄호 ()를 쓰지 않아도 돼요.

reversedNames = names.sorted { $0 > $1 }

후행 클로저는 클로저가 충분히 길어서 한 줄에 인라인으로 못 쓸 때 가장 유용해요. 예를 들어 Swift의 Array 타입에는 map(_:) 메서드가 있는데, 이 메서드는 클로저 표현식을 단일 인자로 받아요. 이 클로저는 배열의 각 항목마다 한 번씩 호출되고, 그 항목에 대한 대체 매핑 값(다른 타입일 수도 있어요)을 돌려주죠. 매핑의 성격과 반환 값의 타입은 map(_:)에 넘기는 클로저 안에 코드를 써서 정해요.

제공한 클로저를 각 배열 요소에 적용한 뒤, map(_:) 메서드는 새 매핑 값들을 모두 담은 새 배열을 돌려줘요. 순서는 원래 배열의 대응 값들과 같죠.

후행 클로저와 함께 map(_:) 메서드를 써서 Int 값 배열을 String 값 배열로 바꾸는 방법을 볼게요. 배열 [16, 58, 510]으로 새 배열 ["OneSix", "FiveEight", "FiveOneZero"]를 만든답니다.

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]

위 코드는 정수 자릿수와 영어 이름 사이의 매핑 사전을 만들고, 문자열로 바꿀 준비가 된 정수 배열도 정의해요.

이제 numbers 배열에 map(_:) 메서드로 클로저 표현식을 후행 클로저로 넘겨서 String 값 배열을 만들 수 있어요.

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(_:) 메서드는 배열의 각 항목마다 클로저 표현식을 한 번씩 호출해요. 클로저의 입력 매개변수 number의 타입은 지정하지 않아도 돼요. 매핑할 배열의 값들에서 타입을 추론할 수 있으니까요.

이 예시에서 변수 number는 클로저의 number 매개변수 값으로 초기화돼요. 그래야 클로저 본문 안에서 그 값을 수정할 수 있으니까요. (함수와 클로저의 매개변수는 항상 상수예요.) 이 클로저 표현식은 반환 타입도 String으로 지정해서, 매핑된 출력 배열에 저장될 타입을 알려줘요.

클로저 표현식은 호출될 때마다 output이라는 문자열을 만들어요. 나머지 연산자(number % 10)로 number의 마지막 자릿수를 계산하고, 이 자릿수로 digitNames 사전에서 알맞은 문자열을 찾죠. 이 클로저는 0보다 큰 정수라면 무엇이든 문자열 표현으로 만들 수 있어요.

참고: digitNames 사전의 서브스크립트 호출 다음에는 느낌표(!)가 붙어 있어요. 사전 서브스크립트는 선택적 값을 돌려주는데, 키가 없으면 조회가 실패할 수 있음을 나타내기 위해서예요. 위 예시에서는 number % 10이 항상 digitNames 사전의 유효한 서브스크립트 키라는 게 보장되므로, 서브스크립트의 선택적 반환 값에 저장된 String 값을 강제로 언래핑하기 위해 느낌표를 썼죠.

digitNames 사전에서 얻은 문자열은 output에 더해져서, 숫자를 역순으로 쌓아 문자열 버전을 만들게 돼요. (표현식 number % 1016에 대해 6, 58에 대해 8, 510에 대해 0을 줘요.)

그다음 number 변수는 10으로 나뉘어요. 정수이므로 나눗셈 중에 아래로 내림이 돼서 161, 585, 51051이 돼요.

이 과정은 number0이 될 때까지 반복돼요. 그 시점에 output 문자열이 클로저로부터 반환되고, map(_:) 메서드가 출력 배열에 추가하죠.

위 예시에서 후행 클로저 문법을 쓴 덕분에, 클로저의 기능을 그것이 지원하는 함수 바로 뒤에 깔끔하게 담을 수 있었어요. 전체 클로저를 map(_:) 메서드의 바깥 괄호 안에 감쌀 필요가 없죠.

함수가 여러 클로저를 받는다면, 첫 번째 후행 클로저의 인자 라벨은 생략하고 나머지 후행 클로저에는 라벨을 붙여요. 예를 들어 아래 함수는 사진 갤러리를 위해 사진을 불러와요.

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

사진을 불러오려고 이 함수를 호출하면 두 클로저를 제공해요. 첫 번째 클로저는 다운로드가 성공한 뒤 사진을 표시하는 완료 핸들러이고, 두 번째 클로저는 사용자에게 오류를 보여주는 오류 핸들러예요.

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

이 예시에서 loadPicture(from:completion:onFailure:) 함수는 네트워크 작업을 백그라운드로 보내고, 네트워크 작업이 끝나면 두 완료 핸들러 중 하나를 호출해요. 이렇게 함수를 작성하면 네트워크 실패를 처리하는 코드와 성공 다운로드 후 사용자 인터페이스를 갱신하는 코드를 말끔히 분리할 수 있어요. 두 상황을 모두 처리하는 클로저 하나를 쓰는 대신에요.

참고: 완료 핸들러는 특히 여러 핸들러를 중첩해야 할 때 읽기 어려워질 수 있어요. 대안으로 비동기 코드를 쓸 수도 있는데, 이는 doc:Concurrency에서 다룹니다.

값 포착하기

클로저는 정의된 주변 맥락의 상수와 변수를 포착할 수 있어요. 그러면 그 상수와 변수의 값을 클로저 본문 안에서 참조하고 수정할 수도 있죠. 심지어 그 상수와 변수를 정의한 원래 스코프가 더 이상 존재하지 않아도 그렇게 할 수 있어요.

Swift에서 값을 포착할 수 있는 클로저의 가장 단순한 형태는, 다른 함수의 본문 안에 작성된 중첩 함수예요. 중첩 함수는 바깥 함수의 인자들을 포착할 수 있고, 바깥 함수 안에 정의된 상수와 변수들도 포착할 수 있죠.

이번엔 makeIncrementer라는 함수의 예시를 볼게요. 이 함수 안에 incrementer라는 중첩 함수가 있어요. 중첩 함수 incrementer()는 주변 문맥에서 runningTotalamount 두 값을 포착해요. 이 값들을 포착한 뒤, incrementermakeIncrementer가 클로저로 돌려주는데, 호출될 때마다 runningTotalamount만큼 증가시켜요.

func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

makeIncrementer의 반환 타입은 () -> Int예요. 이는 단순한 값이 아니라 함수를 돌려준다는 뜻이죠. 돌려주는 함수는 매개변수가 없고, 호출될 때마다 Int 값을 반환해요. 함수가 다른 함수를 반환하는 방법을 배우려면 doc:Functions#Function-Types-as-Return-Types를 확인하세요.

makeIncrementer(forIncrement:) 함수는 runningTotal이라는 정수 변수를 정의해서, 반환될 인크리멘터의 현재 누계를 저장해요. 이 변수는 0으로 초기화되죠.

makeIncrementer(forIncrement:) 함수는 Int 매개변수 하나를 갖는데, 인자 라벨은 forIncrement, 매개변수 이름은 amount예요. 이 매개변수로 전달되는 인자 값은 반환된 인크리멘터 함수가 호출될 때마다 runningTotal을 얼마나 증가시킬지를 정해요. makeIncrementer 함수는 실제 증가를 수행하는 incrementer라는 중첩 함수를 정의하는데, 이 함수는 단순히 amountrunningTotal에 더하고 결과를 반환해요.

중첩 함수 incrementer()를 혼자 떼어 놓고 보면 조금 특이해 보일 수 있어요.

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer() 함수는 매개변수가 없는데도 함수 본문 안에서 runningTotalamount를 참조해요. 주변 함수에서 runningTotalamount에 대한 참조를 포착해서 자기 함수 본문 안에서 쓰는 방식으로 말이죠. 참조로 포착하기 때문에 makeIncrementer 호출이 끝나도 runningTotalamount가 사라지지 않아요. 또 다음에 incrementer 함수를 호출할 때도 runningTotal을 사용할 수 있게 해주죠.

참고: 최적화로, Swift는 클로저가 값을 변경하지 않고, 클로저가 만들어진 뒤에도 값이 변경되지 않는 경우에는 값의 복사본을 포착해 저장할 수도 있어요.

또한 Swift는 더 이상 필요 없어진 변수를 정리하는 데 관련된 모든 메모리 관리를 처리해요.

makeIncrementer가 실제로 동작하는 예시를 볼게요.

let incrementByTen = makeIncrementer(forIncrement: 10)

이 예시는 incrementByTen이라는 상수가 호출될 때마다 자기 runningTotal 변수에 10을 더하는 인크리멘터 함수를 가리키도록 만들어요. 함수를 여러 번 호출하면 그 동작을 실제로 확인할 수 있어요.

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

두 번째 인크리멘터를 만들면, 그것은 새롭고 별개의 runningTotal 변수에 대한 자기만의 저장된 참조를 갖게 돼요.

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

원래 인크리멘터(incrementByTen)를 다시 호출하면 자기 runningTotal 변수를 계속 증가시켜요. incrementBySeven이 포착한 변수에는 영향을 주지 않죠.

incrementByTen()
// returns a value of 40

참고: 클래스 인스턴스의 프로퍼티에 클로저를 할당하고, 그 클로저가 인스턴스 또는 그 구성원을 참조해서 인스턴스를 포착한다면, 클로저와 인스턴스 사이에 강한 참조 순환(strong reference cycle)이 생겨요. Swift는 이런 강한 참조 순환을 깨기 위해 *캡처 리스트(capture lists)*를 사용해요. 자세한 내용은 doc:AutomaticReferenceCounting#Strong-Reference-Cycles-for-Closures에서 확인하세요.

클로저는 참조 타입

위 예시에서 incrementBySevenincrementByTen은 상수인데도, 이 상수들이 가리키는 클로저는 여전히 포착한 runningTotal 변수들을 증가시킬 수 있었어요. 이는 함수와 클로저가 참조 타입이기 때문이에요.

함수나 클로저를 상수나 변수에 할당할 때마다, 실제로는 그 상수나 변수를 함수나 클로저에 대한 참조로 설정하는 거예요. 위 예시에서 상수인 것은 incrementByTen가리키는 클로저의 선택이지, 클로저 자체의 내용물이 아니에요.

또한 이는 클로저를 두 개의 서로 다른 상수나 변수에 할당하면, 두 상수·변수가 모두 같은 클로저를 가리킨다는 뜻이기도 해요.

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

위 예시는 alsoIncrementByTen을 호출하는 것이 incrementByTen을 호출하는 것과 같다는 걸 보여줘요. 둘 다 같은 클로저를 가리키므로, 둘 다 같은 누계를 증가시키고 반환하죠.

탈출 클로저

클로저가 함수에 인자로 전달되지만, 함수가 반환된 뒤에 호출된다면 그 클로저는 함수를 *탈출(escape)*한다고 말해요. 클로저를 매개변수 중 하나로 받는 함수를 선언할 때, 매개변수 타입 앞에 @escaping을 써서 클로저가 탈출해도 된다는 것을 나타낼 수 있어요.

클로저가 탈출하는 한 가지 방법은 함수 바깥에 정의된 변수에 저장되는 거예요. 예를 들어 비동기 작업을 시작하는 함수들은 대부분 완료 핸들러로 클로저 인자를 받아요. 함수는 작업을 시작한 뒤 반환하지만, 클로저는 작업이 끝날 때까지 호출되지 않죠. 다시 말해 클로저는 탈출해서 나중에 호출되어야 해요. 예를 들면 이래요.

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_:) 함수는 클로저를 인자로 받아서, 함수 바깥에서 선언된 배열에 추가해요. 만약 이 함수의 매개변수를 @escaping으로 표시하지 않았다면 컴파일 타임 오류가 났을 거예요.

self를 참조하는 탈출 클로저는, self가 클래스의 인스턴스를 가리킬 때 특별히 주의가 필요해요. 탈출 클로저에서 self를 포착하면 실수로 강한 참조 순환을 만들기 쉬워지죠. 참조 순환에 대한 정보는 doc:AutomaticReferenceCounting을 확인하세요.

보통 클로저는 본문에서 그 변수를 사용함으로써 변수를 암시적으로 포착해요. 하지만 이 경우에는 명시적으로 해야 해요. self를 포착하려면 사용할 때 self를 명시적으로 쓰거나, self를 클로저의 캡처 리스트에 포함시키면 돼요. self를 명시적으로 쓰면 의도를 표현할 수 있고, 참조 순환이 없는지 확인하라는 것을 스스로 일깨워 주죠. 예를 들어 아래 코드에서 someFunctionWithEscapingClosure(_:)에 전달된 클로저는 self를 명시적으로 참조해요. 반면 someFunctionWithNonescapingClosure(_:)에 전달된 클로저는 비탈출 클로저라서 self를 암시적으로 참조할 수 있어요.

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".

다음은 self를 클로저의 캡처 리스트에 포함시켜 포착하고, 그 뒤에 self를 암시적으로 참조하는 doSomething() 버전이에요.

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

self가 구조체나 열거형의 인스턴스라면 항상 self를 암시적으로 참조할 수 있어요. 하지만 self가 구조체나 열거형의 인스턴스일 때는 탈출 클로저가 self에 대한 변경 가능한 참조를 포착할 수 없어요. 구조체와 열거형은 공유 변경을 허용하지 않거든요. 이는 doc:ClassesAndStructures#Structures-and-Enumerations-Are-Value-Types에서 다룹니다.

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // OK
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

위 예시의 someFunctionWithEscapingClosure 함수 호출은 오류예요. 변경하는 메서드(mutating method) 안에 있으므로 self가 변경 가능한데, 이는 구조체에서 탈출 클로저가 self의 변경 가능한 참조를 포착할 수 없다는 규칙을 위반하기 때문이에요.

자동 클로저

*자동 클로저(autoclosure)*는 함수에 인자로 전달되는 표현식을 감싸기 위해 자동으로 만들어지는 클로저예요. 인자를 받지 않고, 호출되면 그 안에 감싸인 표현식의 값을 돌려줘요. 이 문법적 편의 덕분에 명시적인 클로저 대신 일반 표현식을 써서 함수 매개변수 주변의 중괄호를 생략할 수 있어요.

자동 클로저를 받는 함수를 호출하는 것은 흔하지만, 그런 함수를 구현하는 것은 흔하지 않아요. 예를 들어 assert(condition:message:file:line:) 함수는 conditionmessage 매개변수에 자동 클로저를 받아요. condition 매개변수는 디버그 빌드에서만 평가되고, message 매개변수는 conditionfalse일 때만 평가되죠.

자동 클로저는 평가를 지연시킬 수 있어요. 안의 코드는 클로저를 호출할 때까지 실행되지 않으니까요. 평가를 지연하는 것은 부작용이 있거나 계산 비용이 큰 코드에 유용해요. 그 코드를 언제 평가할지 제어할 수 있기 때문이죠. 아래 코드는 클로저가 평가를 어떻게 지연하는지 보여줘요.

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 배열의 첫 요소가 클로저 안 코드에 의해 제거되기는 하지만, 실제로 배열 요소가 제거되는 시점은 클로저가 실제로 호출될 때예요. 클로저가 결코 호출되지 않으면 클로저 안의 표현식은 절대 평가되지 않고, 따라서 배열 요소도 절대 제거되지 않죠. customerProvider의 타입은 String이 아니라 () -> String이라는 점에 주목하세요. 매개변수 없이 문자열을 반환하는 함수인 거죠.

클로저를 함수에 인자로 전달할 때도 똑같이 지연 평가라는 동작을 얻어요.

// 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(customer:) 함수는 고객의 이름을 반환하는 명시적 클로저를 받아요. 아래 serve(customer:) 버전은 같은 동작을 하지만, 명시적 클로저를 받는 대신 매개변수 타입을 @autoclosure 속성으로 표시해서 자동 클로저를 받아요. 이제 함수를 클로저 대신 String 인자를 받는 것처럼 호출할 수 있죠. customerProvider 매개변수의 타입이 @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!"

참고: 자동 클로저를 과용하면 코드를 이해하기 어려워질 수 있어요. 문맥과 함수 이름만으로 평가가 지연되고 있다는 것이 분명해야 해요.

탈출이 허용되는 자동 클로저를 원한다면, @autoclosure@escaping 속성을 함께 쓰면 돼요. @escaping 속성은 위의 doc:Closures#Escaping-Closures에서 설명했어요.

// 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!"

위 코드에서 collectCustomerProviders(_:) 함수는 자신에게 전달된 클로저를 customerProvider 인자로 호출하는 대신, 그 클로저를 customerProviders 배열에 추가해요. 이 배열은 함수의 스코프 바깥에서 선언되어서, 배열 안의 클로저들은 함수가 반환된 뒤에도 실행될 수 있어요. 그 결과 customerProvider 인자의 값은 함수의 스코프를 탈출하는 것이 허용되어야 하죠.

더 알아보기

클로저를 시작점으로 삼아 Swift의 함수형 프로그래밍 감각을 익혀 보세요. 함수의 타입과 반환(함수 타입을 반환 타입으로 쓰기), 그리고 값 타입으로서의 구조체·열거형을 짚고 가면, 클로저가 값을 포착·참조로 다루는 방식이 더 선명하게 보여요. 클로저가 클래스 인스턴스를 포착할 때 생기는 강한 참조 순환과 캡처 리스트도 doc:AutomaticReferenceCounting에서 이어서 다뤄요. 탈출 클로저의 완료 핸들러가 중첩될수록 읽기 어려워진다면, doc:Concurrency의 비동기 코드를 대안으로 살펴보세요.