안녕하세요~ 차니에요!
오늘은 Swift의 클로저에 대해 알아보도록 하겠습니다.
1. 클로저(Closure)란?
일정 기능을 수행하는 코드의 블럭을 뜻하며 일급 객체의 역할을 할 수 있다.
- Named Closure : 이름이 있는 클로저로 일반적인 함수가 이에 해당합니다.
- Unnamed Closure : 이름이 없는 익명 함수로 일반적으로 클로저라 함은 Unnamed Closure를 지칭합니다.
Swift에서 함수는 일급 객체이므로 클로저(익명 함수) 또한 일급 객체로서의 역할이 가능합니다.
2. 클로저 표현 방식
{ (Parameters) -> Return Type in
// Code ...
}
아래와 같이 함수로 따로 정의된 형태가 아닌 인자로 들어가 있는 형태를 Inline Closure 라고 합니다.
let list = [15, 23, 12, 33, 46]
let sorted = list.sorted(by: { (v1: Int, v2: Int) -> Bool in
return v1 < v2
})
print(sorted) // 12, 15, 23, 33, 46
클로저(익명 함수) 안에 파라미터는 (v1, v2) 리턴 타입은 Bool 형태이며 배열의 값들을 오름차순 정렬하는 기능을 수행하는 클로저입니다.
3. 클로저 축약
클로저는 메서드의 특징을 알고 있다면 축약하여 사용할 수 있습니다. (클로저 경량 문법)
클로저 축약은 코드 가독성을 높여주지만 과도한 축약은 오히려 이해하기 난해한 코드가 될 수 있으므로 적당히 사용하는 것이 좋습니다!
3-1. 타입 생략
let sorted = list.sorted(by: { (v1, v2) in
return v1 < v2
})
타입 추론을 통해 list 변수의 자료형(Type)이 Int 임을 알 수 있으므로 파라미터 타입을 생략하여 사용할 수 있습니다.
마찬가지로 반환 타입도 생략 가능합니다.
3-2. 반환 키워드 생략
let sorted = list.sorted(by: { (v1, v2) in
v1 < v2
})
클로저 내부의 코드가 한 줄이라면 return 키워드를 생략하여 표현할 수 있습니다.
3-3. 파라미터 이름 생략
let sorted = list.sorted(by: {
$0 < $1
})
파라미터 이름을 생략하여 표현할 수 있습니다.
코드 블럭 안에서 $0 ... $n 까지 파라미터를 순서대로 호출하여 사용 가능합니다.
3-4. 연산자 함수
let sorted = list.sorted(by: <)
연산자를 사용할 수 있는 타입의 경우 연산자만 표현하여 생략이 가능합니다.
3-5. 후행 클로저 (Trailing Closure)
let sorted = list.sorted {$0 < $1}
함수의 마지막 인자가 클로저라면, 함수 호출 () 괄호 대신 {} 중괄호를 사용하여 호출이 가능합니다.
이때 argument label(by: )은 생략됩니다.
4. 클로저 값 캡쳐(Capturing Values)
func incrementer(v: Int) -> () -> Int {
var amount = 0
func plus() -> Int {
amount += v
return amount
}
return plus
}
let plusTwo = incrementer(v: 2)
print(plusTwo()) // 2
print(plusTwo()) // 4
print(plusTwo()) // 6
클로저에서 캡쳐란 인스턴스의 프로퍼티에 접근하거나 인스턴스의 메소드를 호출하는 것을 캡쳐라고 합니다.
클로저의 값 캡쳐는 주소 참조입니다. plusTwo 변수는 같은 클로저를 참조하기 때문에 값이 계속해서 증가되는 모습을 볼 수 있습니다.
4-1. 클로저의 강한 순환 참조(strong reference cycle)
만약 클로저를 어떤 클래스 인스턴스의 프로퍼티로 할당하고 그 클로저가 그 인스터스를 캡쳐하면 강한 순환 참조가 발생합니다.
쉽게 말해 서로가 서로를 참조하여 참조 카운트(Refernce Count; RC)가 0이 되지 않아서 메모리가 해제되지 않고 Heap 영역에 계속 남아있는 것을 의미합니다.
class Person {
var name:String = ""
lazy var getName: () -> String? = {
return self.name
}
init(name: String) {
self.name = name
}
deinit {
print("Deinit Person")
}
}
getName의 클로저에서 self.name의 값을 캡쳐하여 강한 순환 참조가 발생하는 예제 코드입니다.
4-2. 강한 순환 참조 해소 방법(weak, unowned)
이를 해소하기 위해 클로저의 선언부에 Capture Lists를 정의하여 약한 순환 참조로 수준을 낮춰주도록 합니다.
lazy var getName: () -> String? = { [weak self] in
return self?.name
}
약한(weak)참조로 캡쳐 리스트를 정의하였습니다.
lazy var getName: () -> String = { [unowned self] in
return self.name
}
미소유(unowned)참조로 캡쳐 리스트를 정의하였습니다.
weak는 옵셔널 타입이고 unowned는 논옵셔널 타입이라는 차이가 있습니다.
4. 클로저의 종류
4-1. 탈출 클로저 (escaping closure)
함수의 인자로 전달한 클로저가 함수가 끝나고 실행되는 것
일반적인 클로저(non-escaping closure)들은 해당 함수가 끝나기 이전에 실행된다는 점, 해당 함수 내부에서만 쓰일 수 있다는 특징이 있었습니다. 이를 "탈출 불가 상태" 라고 표현합니다. 반대로 "탈출 클로저"는 함수 외부에서 쓰일수 있도록 탈출 시키는 클로저입니다.
주로 비동기 처리가 필요한 네트워크 통신 등에서 많이 쓰입니다.
func printName(_ closure: @escaping (String) -> ()) {
let name = "channy"
print("func start")
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
closure(name)
}
print("func end")
}
클로저 앞에 @escaping으로 탈출 클로저임을 명시해줍니다.
함수 실행 10초 후 탈출 클로저를 실행하는 코드입니다.
printName { (name) in
print(name)
}
Alamofire.request(urlRequest).responseJSON { response in
}
대표적으로 네트워크 통신에 쓰이는 Alamofire 라이브러리를 보면 위와 같은 구조입니다.
4-2. 자동 클로저 (auto closure)
함수의 인자로 전달되는 코드를 블럭으로 감싸 자동으로 클로저로 만들어주는 것을 의미합니다.
var kakaoFriends = ["muzi", "ryon", "apeach", "neo", "tube", "con"]
func removeAt(_ closure: @autoclosure () -> String) -> String {
return closure()
}
print(removeAt(kakaoFriends.removeFirst())) // "muzi"
자동 클로저를 적용한 코드입니다. 클로저 앞에 @autoclosure 임을 명시합니다.
{} 중괄호 없이 바로 메서드를 넘겨줄 수 있습니다.
func removeAt(_ closure: () -> String) -> String {
return closure()
}
print(removeAt({
kakaoFriends.removeFirst() // "muzi"
}))
일반 클로저 코드입니다.
{} 중괄호를 사용하여 파라미터로 클로저를 넘겨주는 모습입니다.
이번 포스팅은 쓰다보니 생각보다 길어졌네요 :D
도움이 되셨다면 공감 한 번 부탁드리겠습니다~!