티스토리 뷰

클로저는 이름을 가지고 있던 가지고 있지 않던 {} 로 묶이는 거의 모든 코드 블록을 뜻한다고 생각하면 된다. 

보통의 경우, 클로저라고 하면 이름이 없는 unnamed Clpsure를 뜻하며 익명함수라고도 한다.

 

클로저도 익명이긴 하지만 함수의 특징을 모두 가지고 있기 때문에 인자나 반환 값을 모두 가질 수 있다.

 

1. 클로저 표현식

 

클로저는 익명함수인 만큼 func 키워드를 사용 않는다. 

{ (parameters) Return Type in 
	Statement
}

위와 같이 표현하며, in 키워드를 기준으로 Closure head 와 Closure Body로 구분할 수 있다.

 

실제로 클로저를 어떻게 선언하고 사용하는 지 확인해보면,

 

 - Parameter와 Return Type 이 둘 다 없는 경우:

let closure = { () -> () in 
	print("Closure")
}

 - 둘 다 가지고 있는 경우: 

let closure = { (name: String) -> String in 
	return "Hello, \(name)"
}

위 예제를 보면, name 이라는 Parameter name 이 사용되었는데, 이는 함수에서의 name = label 개념과는 조금 다르다. 

클로저는 기본적으로 paramter label을 가지고 있지 않기 때문에 호출 시에 label을 붙여 사용하게 되면 에러가 발생한다.


2.  클로저 특징

클로저 또한 함수이기 때문에 함수가 가지고 있는 특징을 모두 가진다고 볼 수 있다.

 

 - 클로저를 변수나 상수에 대입 가능

 

 - 함수의 파라미터 타입으로 클로저를 전달할 수 있다.

func doSomething(closure: () -> ()) {
	closure()
}

예를 들어, 위와 같이 함수를 파라미터로 전달받아 void를 리턴하는 함수가 있다면 그대로 함수를 하나 선언한 뒤에 파라미터로 넘겨줘도 상관이 없지만, 아래와 같이 클로저를 선언부에서 생성하여 넘겨줘도 된다.

doSomething(closure: { () -> () in 
	print("Hello")
})

즉, 아래와 같은 형태의 코드를 짤 수 있다는 뜻이다.

func doSomething(closure: () -> ()) -> (String) {
    closure()
    
    return "Choonham!"
}

let choonham = doSomething(closure: { () -> () in
    print("Hello, ")
})

print(choonham)

// Hello, 
// Choonham!

 

 - 함수의 반환 타입으로 클로저를 사용할 수 있다.

말 그대로 아래와 같이 함수의 반환 타입으로 클로저를 사용할 수 있다는 말이다.

func doSomething () -> (String) -> (String) {
    let name = "choonham"
    
    return { (name: String) -> (String) in
        return "Hello, \(name)"
    }
}

let doSome = doSomething()

print(doSome("haha"))

 

- 클로저의 직접 실행

클로저를 변수나 상수에 대입하지 않고도 직접 실행할 수 있는데, 아래와 같이 괄호로 묶고 호출 구문을 추가하면 된다.

({ () -> () in
	print("Hello, Choonham!")
})()

3. 클로저의 문법 경량화

- 트레일링 클로저

트레일링 클로저는 함수의 마지막 파라미터가 클로저일 때, 이를 파라미터 값 형식이 아닌 함수 뒤에 붙여 작성하는 문법을 뜻한다. 

이때 Argument Label은 생략된다.

 

위 예제를 하나 가져와보면,

func doSomething(closure: () -> ()) {
	closure()
}

doSomething(closure: { () -> () in 
	print("Hello!")
})

클로저 하나만 파라미터로 받는 함수를 위와 같이 in-line형식으로 선언한다면, 트레일링 클로저를 사용하여 아래와 같이 함수의 가장 마지막에 클로저를 꼬리처럼 덧붙여서 사용할 수 있다.

doSomething () { () -> () in
	print("Hello")
}

이 때, 해당 클로저가 함수의 유일한 파라미터일 경우에는 호출 구문인 ()도 생략할 수 있다.

doSomething{ () -> () in
	print("Hello")
}

 

 - 클로저의 경량 문법

클로저는 필요한 문법을 최소화 하여 위 서술한 예제보다 훨씬 단순하게 사용이 가능하다.

 

아래와 같은 함수가 있다고 가정해보자.

func doSomething(closure: (Int, Int, Int) -> Int) {
    closure(1, 2, 3)
}

기본적인 클로저의 형식을 사용하여 위 함수를 호출하려면, 

func doSomething(closure: (Int, Int, Int) -> Int) {
    closure(1, 2, 3)
}

doSomething(closure: { (a: Int, b: Int, c: Int) -> Int in
    return a + b + c
})

이와 같이 클로저를 full로 작성해야 했지만, 경량 문법을 사용하면 상황에 따라 몇 가지 표현들을 생략할 수 있다.

 

 1) 파라미터 형식과 리턴 형식의 생략

func doSomething(closure: (Int, Int, Int) -> Int) {
    closure(1, 2, 3)
}

doSomething(closure: { (a, b, c) in
    return a + b + c
})

 

 2) 파라미터 이름을 Shortand Argument Names로 대체하고, 이 경우 파라미터 이름과 in 키워드의 생략이 가능하다

조금 생소한데, 위 예제에서 a, b, c 라는 파라미터 이름 대신에 아래와 같이 $와 index를 사용하여 인자의 순서로 사용하는 것이다.

doSomething(closure: {
	return $0 + $1 + $2
})

 

 3) 단일 리턴문만 남을 경우, return의 생략이 가능

위 예제처럼 클로저 내부가 단일 리턴문 한줄만 가지고 있는 경우, return 문의 생략 또한 가능하다.

doSomething(closure: {
	$0 + $1 + $2
})

 

이제 위에서 서술한 트레일링 클로저까지 사용을 한다면, 클로저 선언을 함수 뒤로 뺀 뒤에 호출 구문도 생략이 가능하며, 최종적으로 아래의 형태의 극한으로 경량화하여 클로저를 사용할 수 있다.

doSomething() {
	$0 + $1 + $2
}

doSomething {
	$0 + $1 + $2
}

4.  값 캡쳐

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있다. 캡쳐는 c++ 의 참조와 비슷한 개념인데, 원본 변수나 상수를 참조하여 가지고 있기 때문에 원본 값이 사라져도 클로저 내부에서 그 값을 활용할 수 있다.

 

아래 중첩 함수 예제를 보자.

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

내부 함수 incrementer() 의 선언부에 있는 변수 runningTotal과 amount는 따로 파라미터로 받지도 않은 incrementer() 입장에서는 참조할 수 없는 '외부' 변수이지만, makeIncrementer() 함수 내부에서 값을 캡쳐하여 저장하기 때문에 사용이 가능하다.

 

이 중첩 함수를 실행해보면, 

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// 10

incrementByTen()
// 20

incrementByTen()
// 30

이상하지 않나? forIncrement 파라미터를 10으로 고정한 뒤 동일한 함수를 3번 실행한 결과는 10, 10, 10을 반환하는 것이 아닌 10, 20, 30을 반환한다. 

 

이는 makeIncrementer() 함수가 내부 변수 runningTotal 과 amount를 캡쳐링하여 함수를 새로 초기화하기 전까지 공유하고 있기 때문이다.

 

위 예제에서 알 수 있는 점은, 클로저는 값을 참조하고 있는 참조 타입이라는 점이다.


5. Escaping Closure

클로저를 함수의 파라미터로 넣을 수 있는데, 함수 밖(함수가 끝나고)에서 실행되는 클로저, 예를 들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저는 파라미터 타입 앞에 @escaping 이라는 키워드를 명시해야 한다.

 

아직 생소한 개념이긴 하지만, 그냥 알고만 넘어가자.

 

아래 코드를 보면,

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

위 함수에서 인자로 전달된 completionHandlers는 someFunctionWithEscapingClosure() 함수가 끝나고 나중에 처리된다. 이 때, @escaping 키워드를 붙이지 않으면 컴파일 시 오류가 발생한다.

 

@escaping 키워드를 사용한 함수의 파라미터의 경우, 아래와 같이 반드시 self. 로 명시해줘야 한다.

var completionHandlers: [() -> Void] = []

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

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)
// Prints "200"

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

6. AutoClosures

autoClosure는 인자 값이 없으며, 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저이다. autoClosure는 실행하기 전까지 실제 실행되지 않기 때문에 복잡한 연산을 하는데 유용하게 사용할 수 있다.

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"

위 예제 코드를 보면 let customerProvider = { customersInLine.remove(at: 0) } 이 클로저 코드를 지났음에도 불구하고 customersInLine.count 는 변함없이 5인 것을 볼 수 있다. 그리고 그 클로저를 실행시킨 이후에서야 값이 하나 제거되어 배열의 원소 개수가 4로 줄어든 것을 확인할 수 있다. 이렇듯 autoClosure는 적혀진 라인 순서대로 바로 실행되는 것이 아니라 실제 사용될 때 지연 호출된다.

 

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

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

 

@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을 같이 사용할 수도 있다. 

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