본문 바로가기

학습 노트/Swift (2021)

061 ~ 066. Closure (클로저)

Closure

Swift에서 closure는 두 가지로 구분 되고, 해당 되는 것은 세 가지 이다.

  • Named Closure
    Function, Nested Function
  • Unnamed Closure
    Anonymous Function

함수와 마찬가지로 클로저 또한 First Class Citizen이다.
클로저와 함수는 선언 방식이 동일하며, 그런 만큼 서로 호환 가능하다.

Syntax

{ (parameters) -> ReturnTypein
    statement
}

{ statements }

parameter에서 returntype까지를 closure head 라고 말 한다.
in 이후를 closure body라 말 한다.
클로저가 극단적으로 짧아지게 되면 brace와 statement만 남게 되는 경우가 있다.

{ print("hello, there") }

 

결과

//error

closure는 global scope에서 단독으로 사용 할 수 없다.

let c = { print("hello, there") }
type(of: c)
결과

() -> ()

클로저의 자료형은 함수와 같이 parameter가 없고, retun type이 없다.

let c = { print("hello, there") } //이름이 없는 closure에 c라고 이름을 정의
c()
결과

hello, there

호출 또한 함수와 비슷하게 이루어진다.

let c2 = { (str: String) -> String in
	return "hello, (str)"
}

let result = c2("there")
print(result)
결과

hellot, there


closure에서는 호출 시 argument label이 필요하지 않다.

let c2 = { (str: String) -> String in
	return "hello, (str)"
}

typealias SSC = (String) -> String

func perform(closure: SSC) {
	print(closure("iOS"))
}

perform(closure: c2)

 

결과

hello, iOS
  1. 함수 perform을 호출한다.
  2. 클로저 타입인 c2를 argument로 전달한다.
  3. perform 함수에서 c2를 parameter로 받아 호출한다.
  4. 함수 perform에서 문자열 iOS를 parameter로 전달한다.
  5. 클로저 c2 에서 처리된 문자열을 반환한다.
  6. 함수 perform에서 문자열을 반환 받아 출력한다.
  7. 함수를 종료한다.
import Foundation

let products = [
	"MacBook Air", "MacBook Pro",
	"iMac", "iMac Pro", "Mac Pro", "Mac mini",
	"iPad Pro", "iPad", "iPad mini",
	"iPhone Xs", "iPhone Xr", "iPhone 8", "iPhone 7",
	"AirPods",
	"Apple Watch Series 4", "Apple Watch Nike+"
]

var mac = products.filter({(name: String) -> Bool in
	return name.contains("Mac")
})

print(mac)

 

결과

["MacBook Air", "MacBook Pro", "iMac", "iMac Pro", "Mac Pro", "Mac mini"]


위 코드에서 closure를 호출하는 것은 사용자가 아닌 filter method이다.

import Foundation

let products = [
	"MacBook Air", "MacBook Pro",
	"iMac", "iMac Pro", "Mac Pro", "Mac mini",
	"iPad Pro", "iPad", "iPad mini",
	"iPhone Xs", "iPhone Xr", "iPhone 8", "iPhone 7",
	"AirPods",
	"Apple Watch Series 4", "Apple Watch Nike+"
]

var mac = products.filter({(name: String) -> Bool in
	return name.contains("Mac")
})

print(mac.sorted())
결과

["Mac Pro", "Mac mini", "MacBook Air", "MacBook Pro", "iMac", "iMac Pro"]


결과의 순서는 일반 사전 순서가 아닌 아스키 코드 기준의 정렬이다.
우리가 원하는 순서의 배열이 아니므로 원하는 방식의 정렬을 선언해 줄 필요가 있다.

import Foundation

let products = [
	"MacBook Air", "MacBook Pro",
	"iMac", "iMac Pro", "Mac Pro", "Mac mini",
	"iPad Pro", "iPad", "iPad mini",
	"iPhone Xs", "iPhone Xr", "iPhone 8", "iPhone 7",
	"AirPods",
	"Apple Watch Series 4", "Apple Watch Nike+"
]

var mac = products.filter({(name: String) -> Bool in
	return name.contains("Mac")
})

print(mac.sorted(by: {(lhs: String, rhs: String) -> Bool in
	return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
}))
결과

["iMac", "iMac Pro", "Mac mini", "Mac Pro", "MacBook Air", "MacBook Pro"]


이런 식으로 클로저를 활용 할 수 있다.
caseInsensitiveCompare등의 method는 문자열 강의에서 정리한다.

연습

함수와 클로저간의 동작 순서 파악하기

typealias SSC = (String) -> String

func perform(closure: SSC) {
	print(closure("iOS"))
}

perform(closure: { (str: String) -> String in
	return "hey, (str)"
})
결과

hi, iOS


함수 호출 시 클로저를 직접 작성해 전달 하는 것도 가능하다.
그리고 이것을 inline closure라고 말 한다.

  1. 함수 perform을 호출한다.
  2. string인 str을 입력 받아 string을 반환하는 클로저를 호출한다.
  3. 함수 perform에서 문자열 iOS를 parameter로 전달한다.
  4. 클로저에서 처리하여 문자열을 반환한다.
  5. 반환 받은 문자열을 출력한다.
  6. 함수를 종료한다.

 

Syntax Optimization

print("start")

DispatchQueue.main.asyncAfter(deadline: .now() + 5, execute: {
	print("end")
})
결과

start
//5초뒤
end
print("start")
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
	print("end")
}
결과

start
//5초뒤
end

두 코드는 완전히 동일한 코드이다.
다만 컴파일러의 도움을 받으면 코드의 모양이 바뀌게 되는데,
스위프트는 코드의 간결함을 중점으로 두고 있으며, 때문에 컴파일러도 최대한 단순한 형태의 코드를 선호한다.
이를 코드 최적화라고 하며, 유독 클로저에서 두드러진다.

문법 최적화 규칙

let products = [
	"MacBook Air", "MacBook Pro",
	"iMac", "iMac Pro", "Mac Pro", "Mac mini",
	"iPad Pro", "iPad", "iPad mini",
	"iPhone Xs", "iPhone Xr", "iPhone 8", "iPhone 7",
	"AirPods",
	"Apple Watch Series 4", "Apple Watch Nike+"
]

print(products.filter({(name: String) -> Bool in
	return name.contains("Watch")
}))

 

결과

["Apple Watch Series 4", "Apple Watch Nike+"]
  • parameter 형식과 return 형을 생략한다.
products.filter({ (name) in
	return name.contains("Watch")
})
  • parameter 이름을 short hand argument name으로 대체한다.
    또한 parameter의 이름과 in 키워드를 생략한다.
products.filter({
	return $0.contains("Watch")
})
  • short hand argument name은 '$'와 argument의 index로 구성된다.
    예를 들어 첫 번째 argument는 $0, 두 번째 argument는 $1 이다.
    단일 return만 남은 경우 return 키워드를 생략한다.
products.filter({
	$0.contains("Watch")
})
  • closure parameter가 마지막 parameter인 경우 trailing closure로 작성한다.
    또한, argument label이 남았다면 삭제한다.
products.filter() {
	$0.contains("Watch")
}
  • '( )' 사이에 parameter가 없다면 '( )'를 생략한다.
products.filter {
	$0.contains("Watch")
}

연습

var mac = products.filter({(name: String) -> Bool in
	return name.contains("Mac")
})

print(mac.sorted(by: {(lhs: String, rhs: String) -> Bool in
	return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
}))

 

결과

["iMac", "iMac Pro", "Mac mini", "Mac Pro", "MacBook Air", "MacBook Pro"]
["iMac", "iMac Pro", "Mac mini", "Mac Pro", "MacBook Air", "MacBook Pro"]
mac.sorted(by: {(lhs, rhs) in
	return lhs.caseInsensitiveCompare(rhs) == .orderedAscending
})
mac.sorted(by: {
	return $0.caseInsensitiveCompare($1) == .orderedAscending
})
mac.sorted(by: {
	$0.caseInsensitiveCompare($1) == .orderedAscending
})
mac.sorted() {
	$0.caseInsensitiveCompare($1) == .orderedAscending
}
mac.sorted {
	$0.caseInsensitiveCompare($1) == .orderedAscending
}

 

결과

["iMac", "iMac Pro", "Mac mini", "Mac Pro", "MacBook Air", "MacBook Pro"]
print(products.contains (where: { (funstr: String) -> Bool in
	return funstr == "iPad"
}))

 

결과

true
products.contains (where: { (funstr) in
	return funstr == "iPad"
})
products.contains (where: {
	return $0 == "iPad"
})
products.contains (where: {
	$0 == "iPad"
})
products.contains () {
	$0 == "iPad"
}
products.contains {
	$0 == "iPad"
}

 

결과

true

 


Capturing Value

Function : 값을 캡처하지 않음
Nested Function : 자신을 포함하고 있는 함수 body에 있는 값에 접근하는 경우 값을 캡처한다.
Anonymous Function : 클로저 외부에 있는 값에 접근 할 때 값을 캡처한다.

var num = 0 
print("check #1: \(num)")

num += 1
print("check #2: \(num)")
결과

check #1: 0
check #2: 1

이 경우 에 출력되는 값은 num 이다.

var num = 0 
let c = { print("check #1: \(num)") }

num += 1
print("check #2: \(num)")
결과

check #1: 0
check #2: 1

결과는 같지만 이 경우 출력되는 값은 클로저 c에서 num을 캡처한 값이다.

var num = 0 
let c = { 
	num += 1
	print("check #1: \(num)") 
}

num += 1
print("check #2: \(num)")
결과

check #1: 1
check #2: 1

swift의 클로저는 참조를 캡처하기 때문에 원본에 직접 작용한다.

클로저에서 값을 캡처 할 때 메모리 관리를 하지 않으면 캡처 사이클 문제가 발생한다.
원인과 관리법은 이후에 정리한다.

 

Escaping Closure

func NonEscaping(closure: () -> ()) {
	print("start")
	closure()
	print("end")
}

NonEscaping {
	print("closure")
}

 

결과

start
closure
end

기본에 충실한 순서대로 출력된다.
함수 body에서 호출 된 클로저는 함수가 종료 되기 전에 실행이 완료 된다.
이를 nonescaping 클로저라고 한다.

func Escaping(closure () -> ()) {
	print("start")
	
	DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
		closure()
	}
	
	print("end")
}

Escaping {
	print("closure")
}

 

결과

//error

closure가 non-escaping 클로저이기 때문에 오류가 발생한다.

func Escaping(closure: @escaping () -> ()) {
	print("start")
	
	DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
		closure()
	}

	print("end")
}

Escaping {
	print("closure")
}
결과

start
end
///3초 뒤
closure

함수 Escaping이 end를 출력한 이후에 closure가 출력 된다.
non-escaping 클로저의 경우 함수가 종료됨과 동시에 클로저가 삭제 된다.
따라서 3초 이후 클로저를 실행하려 했을 때 클로저가 남아있지 않는 경우가 발생한다.
escaping 클로저의 경우 함수가 종료 되더라도 클로저가 실행 될 때 까지 클로저를 삭제하지 않는다.

func Escaping(closure: @escaping () -> ()) {
	print("start")
	
	var a = 12
	
	DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
		closure()
		print(a)
	}

	print("end")
}

Escaping {
	print("closure")
}
결과

start
end
closure
12

원래대로라면 함수 Escaping이 종료됨과 동시에 캡처된 값 a가 삭제되어야 한다.
하지만 함수 종료 이후 클로저가 종료 될 때 까지 남아 있는 것을 확인 할 수 있다.
escaping 클로저는 함수의 생명 주기를 벗어날 수 있음과 동시에 자신이 참조한 값의 생명 주기에도 영향을 미친다.

 

Autoclosure

parameter로 전달 되표현식을 클로저로 맵핑해 주는 특성이다.

func random() -> Int { //0~100 사이의 무작위 수를 반환하는 함수
	return Int.random(in: 0...100)
}

func takeResult(param: Int) { //전달 된 정수를 출력하는 함수
	print(#function)
	print(param)
}

takeResult(param: random())
print("-------------------------------")

 

결과

takeResult(param:)
86 (random)
-------------------------------
  1. 함수 takeresult가 호출 되며, 함수 random이 동시에 호출 된다.
  2. random의 반환 값이 param이 되어 전달된다.
  3. 출력 후 함수를 종료한다.
func takeClosure(param: () -> Int) {
	print(#function)
	print(param())
}

takeClosure(param: { Int.random(in: 0...100) })
print("-------------------------------")

 

결과

takeClosure(param:)
29 //random
-------------------------------

함수 takeClosure는 입력이 없고, 정수를 반환하는 함수형을 가진다.
따라서 클로저를 전달 할 수 있고, 실제로 takeClosuer 호출 하면서 inline 클로저를 전달하고 있다.

  1. takeClosure를 호출한다.
  2. param의 inline 클로저를 전달한다.
  3. 함수 내의 클로저 호출 시 클로저를 실행한다.
  4. 결과를 출력하고 종료한다.

이 때 눈여겨 볼 점은 3번이다.
만약 다음과 같이 함수 내에서 클로저를 호출하지 않는다면, 전달 된 클로저는 실행되지 않는다.

func takeClosure(param: () -> Int) {
	print(#function)
	//print(param())
}

takeClosure(param: { Int.random(in: 0...100) })
print("-------------------------------")

 

결과

takeClosure(param:)
-------------------------------
func takeAutoClosure(param: @autoclosure () -> Int) { //auto-closer 속성 추가
	print(#function)
	print(param())
}

takeAutoClosure(param: { Int.random(in: 0...100) })

 

결과

//error

auto-closure 속성이 추가된 경우 parameter로 클로저를 전달 할 수 없다.
따라서 코드는 아래와 같이 수정 되어야 한다.

func takeAutoClosure(param: @autoclosure () -> Int) { //auto-closer 속성 추가
	print(#function)
	print(param())
}

takeAutoClosure(param: Int.random(in: 0...100))
결과

takeAutoClosure(param:)
72 //random

auto 클로저는 표현식을 자동으로 클로저로 변환 한다.
이렇게 전달하면 random method의 결과가 argument로 사용 되는 것이 아닌,
변환 된 클로저의 결과가 argument로 사용 된다.

코드의 작성이 간편해 지지만, 미리 파악하지 못하는 경우 오류를 유발 할 수 있다.
따라서 사용에는 주의가 필요하다.

let rnd = random()
assert(rnd > 30) //rnd가 30 이하인 경우 종료

 

결과

31 //rnd가 30 이상인 경우

assert는 autocloser를 사용하는 대표적인 함수로,
위의 코드에서 'rnd > 30'는 호출시 바로 실행 되는 것이 아닌 클로저로 변환 된 후 assert 함수 내에서 호출시에 실행된다.

func takeAutoClosure(param: @autoclosure () -> Int) {
	print(#function)
	
	DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
		print(param())
	}
}

takeAutoClosure(param: Int.random(in: 0...100))

 

결과

//error

non-escaping 오류가 발생하는 것으로 auto 클로저는 기본적으로 non-escaping 클로저 라는 것을 알 수 있다.
이 경우 코드를 다음과 같이 수정해 해결 할 수 있다.

func takeAutoClosure(param: @autoclosure @autoclosure () -> Int) {
	print(#function)
	
	DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
		print(param())
	}
}

takeAutoClosure(param: Int.random(in: 0...100))

 

결과

takeAutoClosure(param:)
//1초 뒤
60 //random

또한 auto 클로저의 parameter 타입은 항상 비어 있어야 한다.

 

Multiple Trailing Closure

func multi(first: () -> (), second: () -> (), third: () -> ()) {
}

multi(first: {}, second: {}) {

}

 

클로저를 여러개 전달 하는 경우 위와 같은 방식으로 전달했다.
이에는 문제가 있는데,

  • inline 클로저가 길어지면 다음과 같이 가독성이 크게 훼손된다.
multi(first: {}, second: {


}) {

}

 

첫번째 클로저는 기존의 방식과 동일하고,
두번째 클로저 부터는 argument label과 함께 작성한다.

func multi(first: () -> (), _ second: () -> (), third: () -> ()) {
}

multi {

} second: {

} third:

}

 

결과

//error

만약 위와 같이 argument label이 생략 되어 있다면, multiple trailing closure 문법은 사용 할 수 없다.
따라서 가급적이면 parameter를 function 타입으로 작성하는 경우 argument label을 생략하지 않는 것이 좋다.

 

연습

//기본형
UIView.aniamte(withDuration: 0.3, animations: {

}) { (finished) in

}

//개선형
UIView.animate(withDuration: 0.3) {

} completion{ (finished) in

}

Log

2021.08.19.
블로그 이전으로 인한 글 옮김 및 수정