Closure

클로저란 사용자의 코드 내에서 전달 기능을 자체적으로 포함하는 기능 블럭입니다. 클로저는 정의된 컨텍스트로부터 상수와 변수에 대한 참조를 캡쳐하고 저장합니다. 이러한 상수와 변수를 클로징 오버라고 합니다. Swift는 캡쳐의 모든 메모리 관리를 처리합니다.

note: 캡쳐가 친숙하지 않더라도 걱정하지 마세요. Capturing Values에 관한 글 읽기.

함수에서 소개한 전역함수와 중첩함수는 사실 클로저의 특별한 케이스 입니다. 클로저에는 세가지 형식이 있습니다.

  • 글로벌 함수는 이름을 가지고 아무런 값도 캡쳐하지 않은 클로저입니다.
  • 중첩함수는 이름을 가지고 그들이 둘러싸인 함수로부터 값을 캡쳐할 수 있는 클로저입니다.
  • 클로저 표현식은 그들이 둘러싸인 컨텍스트로 부터 값을 캡쳐할 수 있는 간단한 구문으로 쓰여진 이름이 없는 클로저입니다.

스위프트의 클로저 표현식은 일반적인 시나리오에서 어수선한 구문을 줄이는 최적화를 통해 간단 명료한 스타일을 가지고 있습니다.  최적화에는 다음이 포함됩니다:

  • 컨텍스트로부터 파라미터와 반환 값 타입 추론
  • 단일 표현식 클로저로부터 암시적 반환
  • 약식형 인자 이름
  • 후치 클로저 구문

클로저 표현식(Closer expression)

중첩함수는 더 큰 함수의 부분으로서 자체 포함된 코드 블럭의 이름을 짓고 정의하기에 편리합니다. 그러나 가끔은 완전한 선언과 이름없이 함수와 비슷한 구조의 짧은 버전을 작성하는게 더 유용할 때가 있습니다. 특히 한개 또는 그 이상의 요소로서 함수를 사용하는 함수나 메소드의 경우 더 그렇습니다.

클로저 표현식은 집중적인 구문을 간략하게 인라인 클로저로 작성하는 방법입니다. 클로저 표현식은 목적과 명료성의 손실 없이 간략화한 형식의 클로저를 작성하기위해 여러 구문의 최적화를 제공합니다. 아래 클로저 표현식 예제는 여러 번 반복에 의해 sorted(by:)메소드의 단일 예제를 정제함으로써 최적화를 나타내며, 각 예제는 같은 기능을 더 간결한 방법으로 표현합니다.

정렬 메소드

스위프트의 기본 라이브러리는 sorted(by:) 메소드를 제공합니다. 이 메소드는 알려진 타입의 값의 배열을 정렬 클로저의 결과값을 기반으로 정렬합니다. 한번 이 정렬 프로세스가 완료되고 나면, sorted(by:) 메소드는 원래의 배열과 같은 타입과 사이즈를 가지고 요소들이 올바른 순서로 정렬된 새 배열을 반환합니다. 원래의 배열은 이 메소드에 의해 수정되지 않습니다.

아래 클로저 표현식 예제에서는 정렬 메소드를 사용해 알파벳의 역순서대로 String 값의 배열을 정렬할 것입니다.

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

정렬 메소드는 배열의 내용과 같은 타입인 두 개의 인자를 사용하는 클로저를 허용하고, 값이 정렬 된 후 첫 번째 값이 두 번째 값의 앞, 뒤 중 어디에 나타나야하는지 Bool 값을 반환합니다. 정렬 클로저는 첫 번째값이 두 번째 값의 이전에 나타나야 한다면 true값을 반환하고, 그 반대의 경우에는 false를 반환합니다.

이 예제는 String 배열을 정렬하기 때문에 정렬 클로저는 (String, String) -> Bool타입이 됩니다.

정렬클로저를 제공하는 한가지 방법은 올바른 타입의 일반 함수에 작성하고, 이 것을 인자 값으로서 정렬 메소드에 전달 하면 됩니다.

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)를 작성하기 위해 이 방법을 쓰기에는 다소 깁니다. 이번 예제에서는, 더 자주 쓰이는 방식인 클로저 표현구를 이용하여 정렬 클로저를 인라인으로 작성하는 것을 알아보겠습니다.

Closure Expression syntax

클로저 표현구는 다음 형식을 따릅니다:

{ (parameters) -> return type in
	statements
}

parameters는 클로저 표현구에서 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 } )

이 것은 정렬 메소드에 대한 전체 호출이 동일하게 유지되었음을 보여줍니다. 소괄호가 메소드에 대한 모든 인자를 감싸고 있지만, 이제 인라인 클로저의 인자입니다.

컨텍스트로부터 타입 추론(Inferring Type From Context)

정렬 클로저가 하나의 인자를 메소드로 보내기 때문에, 스위프트는 이 클로저의 인자의 타입과 반환 값의 타입을 추론할 수 있습니다. 정렬 메소드는 string 배열로 호출되며, 인자는 (String, String) -> Bool 타입의 함수가 됩니다. 이것은 (String, String)Bool 타입이 클로저 표현식 정의의 한 부분으로서 쓰여질 필요가 없다는 것을 뜻합니다. 모든 타입들은 추론이 가능하기 때문에 반환 화살표(->)와 괄호 주변 파라미터들의 이름들 또한 생략할 수 있습니다:

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

인라인 클로저 표현구로서 함수나 메소드에 클로저를 넘길 때 파라미터 타입들과 반환값 타입은 항상 추론이 가능합니다. 결과적으로 클로저가 함수나 메소드 인자로서 사용될 때, 완전한 형식으로 인라인 클로저를 작성할 필요가 없습니다.

그렇지만 다른 사람들이 코드를 읽을 때 타입에 대해 오해하지 않게 하도록 타입을 명확하게 만들어 놓아도 됩니다. 정렬 메소드의 경우에, 클로저의 목적이 분명하고 문자열 배열의 정렬을 수행하기 떄문에 코드를 보는 사람이 클로저가 string 값으로 추측하더라도 안전합니다.

단일 표현식 클로저의 암시적 반환(Implicit Returns from Single-Expression Closures)

단일 표현식 클로저는 이전 예제 버전에서 return 키워드를 생략함으로써 단일 표현식을 이용해 암시적으로 값을 반환 할 수 있습니다:

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

여기서 sorted(by:)메소드 인자의 함수 타입은 클로저에 의해 Bool 값이 반환 될 수밖에 없습니다. 왜냐하면 클로저의 바디 부분이 Bool 값을 리턴하는 단일 표현식(s1 > s2)을 포함하고 있기 때문에 명확하고, return 키워드는 생략이 가능해집니다.

축약형 인자 이름

스위프트는 자동으로 인라인 클로저에 축약형 인자 이름을 제공합니다. 이 것은 $0, $1, $2 등의 형태이며, 클로저의 인자값으로 참조할 때 사용할 수 있습니다.

만약 이 축약형 인자 이름을 클로저 표현식 내에서 사용한다면, 정의할 때 클로저의 인자 리스트를 생략할 수 있습니다. 축약형 인자 이름의 타입은 함수 타입에서 유추 되고, 축약형 인자의 최대 수가 클로저에서 사용하는 인자의 개수를 결정합니다. 클로저 표현식이 바디 전체를 구성하기 때문에, in 키워드는 또한 생략할 수 있습니다.

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

여기서, $0$1은 클로저의 첫 번째, 두 번째 String인자를 참조합니다. $1은 축약형 인자중에 가장 높은 숫자임으로, 클로저는 2개의 인자가 사용되고 있다고 이해하게 됩니다. sorted(by:) 함수는 여기서 두 인자 모두 문자열을 가진 클로저라고 추측하기 때문에, 축약형 인자 $0$1은 모두 String 타입이 됩니다.

연산자 메소드(Operator Methods)

위의 클로저 표현식 보다 더 짧게 작성하는 방법도 있습니다. 스위프트의 String 타입은 ‘이상’ 연산자( > )가 문자열 구분을 위해 사용될 때, 2개의 String 타입 파라미터가 있고 Bool 타입의 반환 값을 갖는 메소드로 정의됩니다. 이는 sorted(by:) 메소드에서 필요한 메소드 타입과 정확히 일치합니다. 그러므로 간단하게 이상 연산자를 전달할 수 있고, 스위프트는 문자열 구분을 원할 것이라 추론 할 것입니다.

reversedNames = names.sorted(by: >)

자세한 내용은 연산자 메서드 (Operator Methods) 를 참고 바랍니다.

후행 클로저(Trailing Closures)

함수의 마지막 인자로서 클로저 표현식을 함수에 전달 해야하는데 클로저 표현식이 길 때, 후행 클로저를 사용하여 작성하면 유용할 수 있습니다. 후행클로저가 함수의 인자더라도 후행 클로저를 함수의 소괄호 다음에 작성합니다. 후행클로저 구문을 사용할 때, 함수 호출 부분에 첫 번째 클로저 인자 라벨을 작성할 필요 없습니다. 함수 호출은 여러 후행 클로저를 포함할 수 있지만, 우선 단일 후행 클로저에 대한 예제 몇가지를 보겠습니다.

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
}

클로저 표현식 구문 부분에서 이야기 했던 문자열 정렬 클로저를 후행 클로저로 작성하려면 괄호 바깥쪽에 작성 하면 됩니다.

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

만약 클로저 표현식이 후행 클로저이면서 함수나 메소드의 유일한 인자일 경우, 함수 호출시 함수나 메소드의 이름 옆에 소괄호를 작성할 필요가 없습니다.

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

후행 클로저는 클로저의 코드의 길이가 길어 한 라인 안에 작성 할 수 없을 때 가장 유용합니다. 그 예로는 스위프트의 배열 타입에는 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]

위 코드는 숫자와 영어 숫자 표기법의 매핑 딕셔너리를 만든 것입니다. 또한 정수형 배열을 정의하고 문자열로 변환할 준비를 합니다.

이제 클로저 표현식을 배열의 map(_:) 메소드에 후행 클로저로서 전달하여 numbers 배열로 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(_:) 메소드는 배열 내의 각 아이템마다 한 번씩 클로저 표현식을 호출합니다.  클로저의 input 파라미터, number의 타입을 특정할 필요 없습니다. 왜냐하면 배열내의 값이 매핑되면서 타입 유추가 가능하기 때문입니다.

이 예제에서는 number 변수가 클로저의 number 파라미터 값으로 초기화 되기 때문에, 값은 클로저 바디 내에서 수정될 수 있습니다.(함수와 클로저의 파라미터는 항상 상수입니다.) 또한  클로저 표현식은 매핑된 출력 배열에 저장 될 타입을 나타내기 위해 String 타입을 반환 타입으로 지정합니다.

클로저 표현식은 호출 될 때마다 output이라는 문자열을 생성합니다. 이것은 number에 마지막 자리 숫자를 나머지 연산자를 이용하여(number % 10) 계산하고, 이 숫자를 통해 digitNames 딕셔너리에서 알맞은 문자열을 찾습니다. 이 클로저는 모든 0 보다 큰 정수에 대한 문자열 표현을 생성하는데 사용됩니다.

digitNames 딕셔너리에서 검색 된 문자열은 숫자버전의 역순으로 효율적으로 빌드되어 output의 맨 앞에 추가됩니다. (number % 10 표현식은 166을, 58이면 8을, 5100을 반환합니다.)

그러면 number 변수는 10으로 나누어집니다. 정수이기 때문에, 나누어 질때 나머지는 버려지고 161이, 585, 51051이 됩니다.

이 과정은 number가 0이 될 때까지 반복되다가 number가 0이 되는 시점에 클로저에 의해 output 문자열을 반환하고 map(_:)메소드에 의해 output 배열에 추가됩니다.

위 예제에서는는 후행클로저 구문을 사용하여 클로저 전체를 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:onFilure:) 함수는 네트워크 작업을 백그라운드로 보내고 네트워크 작업이 완료되었을 때, 2개의 완료 처리기 중 하나를 호출합니다. 함수를 이 방법으로 작성하면, 두가지 상황을 처리하는 하나나의 클로저를 사용하는 것 대신에, 다운로드를 성공하고 나면 유저 인터페이스를 업데이트 하는 코드에서 네트워크 오류를 처리하는 코드를 명확히 분리합니다.