본문 바로가기
iOS/Swift

[Swift] Closure

by Jiseong 2021. 9. 24.

 

클로저(Closure).. Swift 사용에 있어서 빠질 수 없는 것이다.

 

클로저란?

클로저(Closure)는 코드블럭으로 C와 Objective-C의 블럭(block)과 다른 언어의 람다(lambda)와 비슷하다.

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

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

 

클로저는 어떤 상수나 변수의 참조를 캡쳐(capture)해 저장할 수있다.

 

Swift에서 클로저 표현은 최적화 되어서 간결하고 명확하다. 이 최적화에는 다음과 같은 내용을 포함한다.

  • 문맥(context)에서 인자 타입(parameter type)과 반환 타입(return type)의 추론
  • 단일 표현 클로저에서의 암시적 반환
  • 축약된 인자 이름
  • 후위 클로저 문법

 

 

클로저 표현 

 

클로저는 일반적으로 아래의 형태를 띈다.

{ (Parameter) -> (Return type) in
  Logic...
}

 

아래처럼 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태의 클로저를 인라인 클로저(inline Closure)라 부른다.

클로저의 몸통(body)은 in 키워드 다음에 시작한다.

 

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

 

위 예제에서 sorted(by:)의 메소드에서 이미 (String, String) -> Bool 타입의 인자가 들어와야 하는지 알기 때문에 타입 명시는 생략 될 수 있다.

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

 

하지만 가독성과 코드의 모호성을 피하기 위해 타입을 명시할 수도 있다.

 

 

반환 키워드 생략

단일 표현 클로저에서는 반환 키워드를 생략할 수 있다.

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

 

 

인자 이름 축약

Swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공한다. 이 인자를 사용하면 인자 값을 순서대로 $0, $1, $2 등으로 사용할 수 있다.

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

 

 

연산자 메소드

Swift의 String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자(>) 를 구현해 두었다.

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

let reversedNames = names.sorted(by: >)

 

후행 클로저

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

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

 

 

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

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

 

 

값 캡쳐

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

Swift에서 값을 캡쳐하는 가장 단순한 형태는 중첩 함수(nested function) 이다.

중첩 함수는 함수의 body에서 다른 함수를 다시 호출하는 형태로 된 함수이다. 

 

예제

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

let incrementByTen = makeIncrementer(forIncrement: 10)
// 함수가 각기 실행 되지만 실제로는 변수 runningTotal과 amount가 캡쳐링 되서 그 변수를 공유하기 때문에 계산이 누적된 결과를 갖는다
print(incrementByTen()) // 10
print(incrementByTen()) // 20
print(incrementByTen()) // 30

let incrementBySeven = makeIncrementer(forIncrement: 7)
// 다른 클로저이기 때문에 고유의 저장소에 runningTotal과 amount를 캡쳐링 해서 사용한다. 그래서 다른 값이 나온더.
print(incrementBySeven()) // 7
print(incrementBySeven()) // 14

 

이 함수는 makeIncrementer 함수 안에서 incrementer 함수를 호출하는 형태로 중첩 함수이다.

 

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

 

 

함수와 클로저를 상수나 변수에 할당할 때 실제로는 상수와 변수에 해당 함수나 클로저의 참조(reference)가 할당된다.

만약 한 클로저를 두 상수나 변수에 할당하면 그 두 상수나 변수는 같은 클로저를 참조하고 있는 것이다.

별개로 C나 C++에 익숙한 사람은 함수 포인터를 저장한다고 생각하시면 이해하기 쉬울것이다.

 

 

최적화를 이유로 Swift는 해당 값이 클로저에 의해 변경되지 않고 클로저가 생성된 후 값이 변경되지 않는 경우 값의 복사본을 캡처하고 저장할 수 있다.

또한 Swift는 더 이상 필요치않는 변수를 처리하는 것과 관련된 모든 메모리 관리를 처리합니다.

 

 

Escaping Closure

 

 

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

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

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

 

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

 

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

 

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

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야함.
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// 200

completionHandlers.first?()
print(instance.x)
// 100

 

 

Auto Closure

자동클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저이다.

자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않는다. 실제 계산이 필요할 때 호출되기 때문에 계산이 복잡한 연산을 하는데에 유용하다.

 

예제

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) } // 호출되지않았으니 넘어감
print(customersInLine.count) // 값변화 X
// 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 함수는 인자로 ()->String(인자가 없고, Strign을 반환하는 클로저)를 가진다.

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

 

 

@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를 선언하면 함수가 이미 클로저 인것을 알기 때문에 인자에 중괄호를 넣지 않아도 된다.

 

Auto Closure를 너무 남용하면 코드를 이해하기 어려워 질 수 있다. 상황이 분명한 경우에만 사용하는 것이 좋다.

 

자동클로저 @autoclosure는 이스케이프 @escaping와 같이 사용할 수 있다.

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []        //  클로저를 저장하는 배열을 선언
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
} // 클로저를 인자로 받아 그 클로저를 customerProviders 배열에 추가하는 함수를 선언
collectCustomerProviders(customersInLine.remove(at: 0))    // 클로저를 customerProviders 배열에 추가
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."        // 2개의 클로저가 추가 됨
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")    // 클로저를 실행하면 배열의 0번째 원소를 제거하며 그 값을 출력
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

 

 

 

 

클로저.. 중요하다... 어렵다....

난 다리가 아프다....

'iOS > Swift' 카테고리의 다른 글

함수 전달인자명과 파라미터명  (0) 2022.02.12
Swift) Optional Binding  (0) 2022.02.10
[Swift] Functions  (0) 2021.09.23
M1 error running pod install  (0) 2021.09.20
text property  (0) 2021.09.10

댓글