본문 바로가기

Language/Swift

Closure (클로저)

이 글은 공부하는 내용을 기반으로 정리하기 위한 목적으로 작성합니다. 언제든 건전한 비판은 환영합니다.

클로저란?

클로저는 함수를 말하는 것이다. 무슨 소리냐면 func 도 함수라고 이야기 할 수 있다. 근데 왜 클로져에 함수라는 이야기가 나오는지????

먼저 클로져를 알기 전에 2가지 클로져가 있다 

named closure, unnamed closure 두 개가 있다.

 

그렇다면 named closure와 un

 

먼저 named closure는 그냥 함수이다

func someThing(){
	print("someThing")
   }

위에 것이 named closure이다. 그냥 이것을 함수라고 부른다

 

unnamed closure는

let someThing = {print("some")}

위에 것이 흔히 클로져라고 통칭되는 unnamed closure이다.

 

클로져 사용방법

먼저 named closure를 쓰다 보면 밑에와 같이 사용하면 된다

func score(a : Int) -> String{
	return "\(a)점"
}

위의 name closure에서 String 부분의 { 괄호를 a 앞으로 빠주고 그 자리에 in을 사용하면 된다

{(a: Int) -> String in
	return "\(a)점"
}

위와 같이 바꿔주면 된다.

물론 위와같이 바꿔주게 되면 받을 수 있는 변수를 넣어주어야 한다.

 

또 다른 방법으로도 클로져를 사용할 수 있게 된다.

let score2 = {(a: Int) -> String in
	return "\(a)점"
}

위에가 받을 수 있는 변수를 선언해 준 것 이다.

 

또한 리턴을 제거 할 수도 있다.

{(a: Int) -> String in
	"\(a)점"
}

위와 같이 사용할 수도 있다. 

또한 리턴 타입도 제외할 수 있다

{(a: Int)  in
	"\(a)점"
}

또한 중괄호( '{' )를 옮길 수도 있다.

let score = (Int) -> String { a in
	"\(a)점"
}

위와 같은 경우는 파라미터를 아예 뒤로 옮기는 것이다

 

또한 파라미터를 사용하지 않더라도 약속된 언어 가지고 사용할 수 있다.

let score: (Int, Int, Int) -> String = {
	"\($0 + $1 + $2)점"
}

파라미터 순으로 들어가는 것이다.

 

 

클로져를 이용해 함수안에 함수를 작성할 수 있는 로직도 있다

예를 들면

let names = ["find","apple","banana","melon","waterMelon"]


let containAlpha:(String, String) -> Bool = {name, find in
	if name.contains(find){
    	return true
    }
	
    return false

}

와 같이 함수를 작성하면 알파벳이 리스트에 있냐 확인할 수 있습니다.

하지만 만약에 저런 알파벳 찾는 것이 아닌 다른 로직인 리스트의 내용을 리턴하는 거라면 코드의 재사용을 위해 함수 자체를 파라미터 값으로 넣을 수 있습니다.

func find(findString : String, condition:(String, String) -> Bool) -> [String]{
	var newName = [String]()
    
    for name in names{
    	if condition(name,findString){
        	newName.append(name)
        }
    }

	return newName
}

위와 같이 condition의 파라미터에다가 함수를 작성할 수 있습니다.

그러면 저 위에를 어떻게 써야 하나?

find(findString : "a", condition: containAlpha)

먼저 작성해 놓은 것은 함수를 기반으로 사용할 수 있습니다.

 

sort 사용법

sort도 unNamed closure 방식으로 사용할 수 있다.

 

예를 들면

var names = ["Alex","chch", "lua", "ancy", "rep"]

위와 같은 배열이 있다고 한다면

 

sort할 수가 있다

원래는 

names.sort{ (lhs, rhs) -> Bool in
	return lhs>rhs
}

위와 같이 사용해야 하지만

축약형을 사용할 수 있다 아래와 같이

names.sort(by : { $0 < $1 } )//인자 이름 축약

names.sort() { $0 < $1 }

names.sort{ $0 < $1 }

names.sort(by : < )//연산자 메소드

 

후위 클로져

후위 클로져를 쓰는 이유는 마지막 인자로 클로져를 넣었을 시에 클로져의 길이가 길다면 후위 클로져를 사용할 수 있습니다.

에를들면 일반적인 클로져의 코드를 짜게 된다면

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

이렇게 넣을 수 있습니다. 이것을 후위 클로져로 바꿔본다면

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

이런 방식으로 바뀔 수 있습니다. ()를 {} 안에 넣어준다면 후행 클로져 방식을 사용할 수 있습니다.

이번에는 후위 클로저를 이용해 숫자(Int)를 문자(String)로 매핑(Mapping)하는 예제를 살펴 보겠습니다. 다음과 같은 문자와 숫자가 있습니다.

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(_:)메소드를 이용해 특정 값을 다른 특정 값으로 매핑하는 할 수 있는 클로저를 구현합니다.

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
}
// let strings는 타입 추론에 의해 문자 배열([String])타입을 갖습니다.
// 결과는 숫자가 문자로 바뀐 ["OneSix", "FiveEight", "FiveOneZero"]가 됩니다.

위 코드는 각 자리수를 구해서 그 자리수를 문자로 변환하고, 10으로 나눠서 자리수를 바꾸며 문자로 변환하는 것을 반복합니다. 이 과정을 통해 숫자 배열을, 문자 배열로 바꿀 수 있습니다. number값은 상수인데, 이 상수 값을 클로저 안에서 변수 var로 재정의 했기 때문에 number값의 변환이 가능합니다. 기본적으로 함수와 클로저에 넘겨지는 인자 값은 상수입니다.

 

값 캡쳐

 

클로저는 특정 문맥의 상수나 변수의 값을 캡쳐할 수 있습니다. 다시말해 원본 값이 사라져도 클로져의 body안에서 그 값을 활용할 수 있습니다. Swift에서 값을 캡쳐 하는 가장 단순한 형태는 중첩 함수(nested function) 입니다. 중첩 함수는 함수의 body에서 다른 함수를 다시 호출하는 형태로 된 함수 입니다. 예제를 보겠습니다.

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

이렇게 보면 익숙치 않아서 어려울 수 있는데 하나하나 뜯어보면 어렵지 않습니다.

먼저 () 부분은 함수를 받겠다는 의미입니다

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

이런 식으로 {} 안에 작성되 있는것을 이야기하며

그 이후에 -> Int는 

return incrementer

 

입니다.

 

클로저는 참조타입

클로져는 복사 타입이 아닌 참조타입 입니다. 클래스를 아시는 분이라면 이게 무슨 뜻인지 알지만 저 나름대로 다시 정리해서 설명해 드리겠습니다.

 

let alsoIncrementByTen = makeIncrement(10)
alsoIncrementByTen()

위에 값 캡쳐에서 한 값에 10을 인자를 넣어주면 당연히 10이 나옵니다.

let alsoIncrementByTen = makeIncrement(10)
let alsoIncrementByTen//20
let alsoIncrementByTen//30
let alsoIncrementByTen//40
let alsoIncrementByTen//50

 

함수를 실행할 수록 값이 커지는 것을 발견할 수 있습니다.

클로져에서 값이 캡쳐 되어서 점점 켜져갑니다.

 

이스케이핑 클로저

 

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

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

그러면 함수 밖에 있는 completionHandlers에 추가 됩니다.

위 함수에서 인자로 전달된 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)
// Prints "200"

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

자동 클로져

자동클로저는 인자 값이 없으며 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저입니다. 자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않습니다. 그래서 계산이 복잡한 연산을 하는데 유용합니다. 왜냐면 실제 계산이 필요할 때 호출되기 때문입니다. 예제를 보면서 무슨 뜻인지 알아 보겠습니다.

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인 것을 볼 수 있습니다. 그리고 그 클로저를 실행시킨 print("Now serving \(customerProvider())!") 이후에야 배열에서 값이 하나 제거되어 배열의 원소 개수가 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) 형, 즉 인자가 없고, String을 반환하는 클로저를 받는 함수 입니다. 그리고 이 함수를 실행할 때는 serve(customer: { customersInLine.remove(at: 0) } )이와 같이 클로저{ 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!"
 
serve함수의 인자를 받는 부분 customerProvider: @autoclosure () 에서 클로저의 인자()앞에 @autoclosure라는 키워드를 붙였습니다. 이 키워드를 붙임으로써 인자 값은 자동으로 클로저로 변환됩니다. 그래서 함수의 인자 값을 넣을 때 클로저가 아니라 클로저가 반환하는 반환 값과 일치하는 형의 함수를 인자로 넣을 수 있습니다. 그래서 serve(customer: { customersInLine.remove(at: 0) } ) 이런 코드를 @autoclosure키워드를 사용했기 때문에 serve(customer: customersInLine.remove(at: 0)) 이렇게 {} 없이 사용할 수 있습니다. 정리하면 클로저 인자에 @autoclosure를 선언하면 함수가 이미 클로저 인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있습니다.
 
자동클로저@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!"
 
collectCustomerProviders함수의 인자 customerProvider @autoclosure이면서 @escaping로 선언되었습니다. @autoclosure로 선언됐기 때문에 함수의 인자로 리턴값 String만 만족하는 customersInLine.remove(at: 0)형태로 함수 인자에 넣을 수 있고, 이 클로저는 collectCustomerProviders함수가 종료된 후에 실행되는 클로저 이기 때문에 인자 앞에 @escaping 키워드를 붙여주었습니다.

 

 

 

 

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

클래스 vs 구조체 차이  (0) 2022.02.19
클래스와 상속  (0) 2022.02.19
Swift 기본 문법 - 자료형  (0) 2022.02.08