오늘의 Swift 상식 (Closure)

장국진
7 min readJul 24, 2019

클로저(Closure)란?

- 클로저는 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 중괄호“{}”로 구분된 코드의 블럭이며, 일급 객체의 역할을 할 수 있다.

- 일급 객체는 전달 인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며, 함수의 반환 값이 될 수도 있다.

- 참조 타입이다.

- 함수는 클로저의 한 형태로, 이름이 있는 클로저이다.

클로저 표현방식

{ (인자들) -> 반환타입 in
로직 구현
}

아래와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저 Inline Closure라고 부른다.

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

클로저의 축약

타입 생략

: 위의 예제에서 sorted(by:)의 경우는 이미 (String, String) -> Bool 타입의 인자가 들어와야 하는지 알고 있기 때문에 클로저에서 타입을 명시하는 것을 생략할 수 있다.

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

하지만 코드의 모호성을 피하기 위해 타입을 명시하는 것이 좋을때도 있다.

반환타입 생략

: 반환 키워드를 생략할 수 있다.

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

인자이름 생략

: 인자 값을 축약해서 사용할 수 있다. (인자의 표기는 $0부터 순서대로)

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

연산자 메소드

: 연산자를 사용할 수 있는 타입의 경우 연산자만 남길 수 있다.

let reversedNames = names.sorted(by: > )

후행클로저

: 인자로 클로저를 넣기가 길다면 후행 클로저를 사용하여 함수의 뒤에 표현할 수 있다.

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

함수의 마지막 인자가 클로저이고, 후행 클로저를 사용하면 괄호“( )”를 생략할 수 있다.

let reversedNames = names.sorted { $0 > $1 }
let reversedNames = names.sorted { (s1: String, s2: String) -> Bool in return s1 > s2 }

값 캡쳐(Capturing Values)

: 클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 즉 원본 값이 사라져도 클로저의 body 안 에서 그 값을 활용할 수 있다.

Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수로 아래는 이에 대한 예시이다.

  • plusTen, plusSeven이 상수이지만 runningTotal을 증가시킬 수 있었던 이유는 클로저가 참조타입이기 때문이다.

함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조가 할당된다. 만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하고 있게되는 것이다.

  • 최적화를 이유로 Swift는 그 값이 클로저에 의해 변경되지 않고, 클로저가 생성된 후 값이 변경되지 않는 경우 값의 복사본을 캡쳐하여 저장한다.
  • 또한 Swift는 변수를 더 이상 필요하지 않을 때 처리하는 모든 메모리 관리를 처리한다.

만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그 클로저가 그 인스턴스를 캡쳐하면 강한 순환 참조에 빠지게 된다. 즉. 인스턴스의 사용이 끝나도 메모리를 해제하지 못하는 것이다. 그래서 swift는 이 문제를 다루기 위해 캡쳐 리스트를 사용한다. (차후 ARC에서 함께 다룰 예정)

Escaping Closure

: 클로저가 함수의 인자로 전달되지만 함수 밖에서 실행되는 것(함수가 반환된 후 실행되는 것)Escape한다고 하며, 이러한 경우 매개변수의 타입 앞에 @escaping이라는 키워드를 명시해야한다. 다음과 같은 경우에 자주 사용된다.

  • 비동기로 실행되는 경우
  • completionHandler(완료에 따른 처리)로 사용되는 클로저의 경우

일반 지역변수가 함수 밖에서 살아있는 것은 전역변수를 함수에 가져와서 값을 주는 것과 다름이 없지만, 클로저의 Escaping은 하나의 함수가 마무리된 상태에서만 다른 함수가 실행되도록 함수를 작성할 수 있다는 점에서 유리하다. 즉, 이를 활용해서 함수 사이에 실행 순서를 정할 수 있다.

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

위 함수에서 인자로 전달된 completionHandler는 someFunctionWithEscapingClosure함수가 끝나고 나중에 처리 된다. 만약 함수가 끝나고 실행되는 클로저에 @escaping키워드를 붙이지 않으면 컴파일시 오류가 발생한다.

@escaping을 사용하는 클로저에서는 self를 명시적으로 언급해야한다.

AutoClosure

: 자동 클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저를 말한다. 자동 클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않는다. 즉 실제 계산이 필요할때 호출이 되기 때문에 계산이 복잡한 연산을 하는데 유용하다.

자동클로저를 함수의 인자 값으로 넣는 예제는 아래와 같다.

Serve 함수는 인자로 ()->String(인자가 없고, Strign을 반환하는 클로저)를 가진다.

그리고 이 함수를 실행할 때 serve(customer: { customersInLine.remove(at: 0) } )와 같이 클로저를 명시적으로 직접 넣을 수 있다.

@autoclosure 키워드를 이용해서 보다 간결하게 사용할 수 있다.

@autoclosure 키워드를 붙임으로써 인자 값은 자동으로 클로저로 변환 된다. 그로인해 인자에 중괄호를 넣지 않아도 된다.

자동클로저를 너무 남용하면 코드를 이해하기 어려울 수 있다! 반드시 autoclosure를 사용하기 분명한 경우에만 사용하자!

@autoclosure는 @escaping과 같이 사용할 수 있다.

고차함수

: 함수의 인자로 다른 함수를 받는 함수

  • 순수 고차함수는 map, filter, reduce를 예로 들 수 있다.
  • 고차 함수를 사용하면 변수로 지정할 필요없이 상수로 지정해서 변환할 수 있다.

map

: 콜렉션 내부의 기존 데이터를 변형하여 새로운 콜렉션 생성

let numbers: [Int] = [2,8,15]
var newNumbers: [Int] = numbers.map { $0 + 1 } // [3, 9, 16]

filter

: 콜렉션 내부의 데이터를 조건에 맞는 새로운 콜렉션으로 생성

let numbers: [Int] = [2,8,15]
var newNumbers: [Int] = numbers.filter { $0 % 2 == 0} // [2, 8]

reduce

: 컨테이너 내부의 콘텐츠를 하나로 통합(ex. Element들의 총합, 총곱 등)

let numbers: [Int] = [2,8,15]
// 초기값이 3에 정수 배열의 모든 값을 더한다.
var sum: Int = numbers.reduce(3) { $0 + $1 } // 28

--

--