일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 플로우차트
- git사용법
- github
- 깃
- APNS
- 스위프트
- Regex
- Git과 Github차이점
- 애플
- 백준
- UIViewController
- 흐름도
- 깃허브
- git
- 순서도
- 플로우 차트
- JSONSerialization
- github란
- 정규표현식
- OS
- xcode
- git이란
- flow chart
- swift
- PushNotification
- ios
- 정규식
- 계산기
- flowchart
- GitvsGithub
- Today
- Total
Diana의 iOS 개발일기
[스위프트 프로그래밍 3판] - 13. 클로저 본문
클로저는 스위프트에서 상당히 중요한 내용임에도 불구하고 상당히 어렵고 사용이 난해하기로 유명합니다.
어렵고 난해하다? 저는 오히려 난해하고 또 난해하다 라고 표현하고 싶습니다. 뭔놈의 표현형식이 그리 많은지...
쉬운 것도 물논 아ㄴ...
각설하고 클로저에 대해 알아보도록 하겠습니다.
[클로저(Closure)]
스위프트의 클로저는 변수나 상수가 선언된 위치에서 참조(Reference)를 획득(Capture)하고 저장할 수 있습니다.
참조를 획득한다? 이게 무슨말일까요?
다시 말해 클로저는 변수와 상수가 메모리에 있던 없던 간에 자신의 내부에서 변수와 상수 값을 참조(Refer)하고 수정할 수 있다는 의미입니다.
이러한 클로저에는 아래와 같이 세 가지 형태가 있습니다.
- 전역함수의 형태: 이름이 있으면서 어떤 값도 획득하지 않는다.
- 중첩된 함수의 형태: 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있다.
- 축약문법의 형태: 이름이 없고 주변 문맥에 따라 값을 획득할 수 있다.
그리고 클로저는 아래와 같은 특징을 가지고 있습니다.
- 클로저는 매개변수와 반환 값의 타입을 생략할 수 있습니다.
- 클로저의 코드가 단 한줄일 경우 이를 반환 값으로 취급하고 return을 생략해줄 수 있습니다
- 축약된 전달인자 이름을 사용할 수 있습니다.
- 후행 클로저 문법을 사용할 수 있습니다.
위의 특징들을 잘 보면 1번과 2번의 특징은 함수의 특징과 비슷한 것을 알 수 있는데 실은 함수는 클로저의 한 형태입니다.
간단한 정리는 여기서 마치고, 저는 위의 설명들 만으로는 클로저가 어떤 기능을 하는지 감이 잡히지 않기 때문에 좀 더 살펴보도록 하겠습니다.
1. 기본 클로저
클로저의 기본 표현 형식은 아래와 같습니다.
{(매개변수들) -> 반환 타입 in 실행코드}
여기서 클로저는 매개변수의 자리에 전달인자를 사용할 수 있고 매개변수의 타입이 앞에 명시된 경우 추론을 통해 매개변수 자리에 타입 명시를 생략해 줄 수 있습니다.
func backwards(first: String, second: String) -> Bool {
print("\(first) \(second) 비교중")
return first > second
}
let reversed: [String] = names.sorted(by: backwards) //클로저가 아닌 일반 구조체의 사용
//------------------------------------------------------------------------------------
let reversed: [String] = names.sorted(by: { (first: String, second: String) -> Bool in
return first > second
}) //클로저
위의 예제는 클로저를 사용하지 않았을 때의 코드와 사용했을 때의 코드를 보여주고 있으며 클로저를 사용했을 때 코드가 좀더 간결해졌음을 알 수 있습니다.
2. 후행 클로저
후행 클로저는 일반 클로저를 좀더 읽기 쉽게 바꾼 것 이라 볼 수 있습니다.
let reversed: [String] = named.sorted(){ (first: String, second: String) -> Bool in
return first > second
}
위의 예제를 보면 클로저가 함수의 괄호가 닫힌 뒤 사용되었고 sorted(by: )의 메서드 처럼 클로저 단 한 개만을 전달인자로 전달하는 경우에는 함수 뒤의 괄호마저 생략이 가능합니다.
3. 클로저 표현의 간소화
클로저는 자체로도 매우 간결하고 가독성이 높지만 위의 표현들에서 좀더 간소화가 가능합니다.
우선 메서드의 전달인자로 전달하는 클로저의 경우 메서드에서 요구하는 타입과 동일해야만 전달이 가능합니다.
따라서 전달인자로 사용되는 클로저의 경우 자동으로 적합한 타입을 준수하였다고 판단되어 매개변수와 반환 값의 타입을 명시해줄 필요가 없습니다.
let reversed: [String] = named.sorted { (first, second) in
return first > second
}
위의 예제의 경우 sorted 메서드는 앞에 정렬에 사용되는 배열이 [String] 이라고 정의되어 있어 String과 String을 전달받아 Bool형식을 반환하는 것이 정해진 함수이고, 따라서 매개변수와 반환 값의 타입을 생략해주었습니다.
두번째로, 클로저는 매개변수의 이름 또한 간결하게 단축 인자로 변경이 가능합니다.
클로저는 첫 번째 전달 인자부터 $에 숫자를 붙여 $0, $1, $2, .... 의 순서를 가진 단축 인자로 표현을 할수 있으며 단축 인자를 사용하였기 때문에 실행코드를 in을 통해 구분해줄 필요성이 사라졌으므로 in 또한 생략이 가능합니다.
let reversed: [String] = names.sorted {
return $0 > $1
}
마지막으로, 클로저는 반환 값을 갖고 실행문이 단 한줄이 경우 해당 실행문이 결국 반환 값과 동일하므로 return 키워드를 생략할 수 있습니다.
이는 함수를 공부할 때 보았던 특징과 비슷함을 알 수 있는데 함수는 클로저의 여러 표현중 하나이다 라는 것을 여기서 새삼 느낄 수 있습니다.
let reversed: [String] = names.sorted { $0 > $1 }
4. 값 획득
위에서 클로저는 참조를 획득(Capture)할 수 있다고 하였습니다.
즉, 클로저는 자신이 정의된 위치 주변의 상수나 변수의 값을 자신의 내부에서 참조하거나 수정할 수 있는데,
이는 비동기 작업에 자주 사용되는 클로저는 비동기 콜백을 작성하는 경우 현재 상태를 미리 획득해 두지 않으면 기능을 실행하는 순간 사용해야 할 상수나 변수가 이미 메모리에 존재하지 않는 경우가 발생할 수 있기 때문에 이를 방지하기 위함입니다.
위의 내용으로는 무슨 의미인지 두루뭉실한 감만 잡히므로 예제를 보도록 하겠습니다.
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
위의 예제는 makeIncrementer 함수 안에 nested 함수인 incrementer 함수가 중첩되어 있는 형태로 incrementer함수는 makeIncrementer 함수로부터 amount와 runningTotal 변수를 참조합니다.
따라서 incrementer 함수가 makeIncrementer 함수의 외부로 빠졌을 경우 필요로 하는 해당 변수들을 참조할 수 없게 되고 따라서 오류가 발생하게 됩니다.
이런 경우 클로저를 사용해 보겠습니다.
let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let first: Int = incrementByTwo() //2
let second: Int = incrementByTwo() //4
let third: Int = incrementByTwo() //6
위의 예제는 incrementByTwo라는 상수 안에 makeIncrementer 함수를 참조해 주었고 클로저는 참조한 변수를 사용하여 코드를 진행하였습니다.
꽤나 헷갈리는 내용인 만큼 다시한번 정리하면, 클로저는 이전에 클래스와 구조체의 차이를 구분하며 언급하였던 참조타입, 값 타입 중 참조타입에 해당합니다. 상수나 변수 안에 함수나 클로저를 할당하게 되면 해당 함수나 클로저의 값을 할당하는 것이 아닌 해당 메모리 주소를 할당해 주는 것이고 메모리 주소가 할당 됨에 따라 할당을 받은 상수나 변수는 이후 함수나 클로저의 변수, 상수 메모리가 해제되어도 필요로 하는 해당 메모리를 직접 가리켜 스스로의 내부에서 메모리를 할당한 뒤 사용이 가능합니다.
let incrementByTwo: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTwo2: (() -> Int) = makeIncrementer(forIncrement: 2)
let incrementByTwo3: (() -> Int) = incrementByTwo2
첫 번째 상수 incrementByTwo와 두번 째 함수 incrementByTwo2에는 각각 따로 메모리가 할당되었습니다.
그리고 incrementByTwo3는 incrementByTwo2에 할당된 메모리를 그대로 받아왔습니다.
이 경우 incrementByTwo와 incrementByTwo2는 완전히 개별적으로 코드가 진행되겠습니다만 incrementByTwo2와 incrementByTwo3는 같은 주소를 가리키고 있으므로 incrementByTwo2로 인해 변경된 내용은 incrementByTwo3에도 반영이 됩니다.
5. 탈출 클로저
비동기 작업을 하는 함수들은 클로저를 컴플리션 핸들러(Completion handler)의 전달인자로 받습니다.
이 때 사용된 클로저는 함수가 종료 된 후 호출을 받는데 @escaping 키워드를 사용하여 탈출이 가능한 클로저임을 명시합니다. 표현 형식은 아래와 같습니다.
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
completionHandlers.append(completionHandler)
}
해당 예제는 someFuctionWithEscapingClosure이라는 이름의 함수이며 매개변수로 completionHandler 클로저를 받습니다.
이 때 completionHandler의 뒤에 차례대로 콜론(:)과 @escaping 키워드를 붙여줌으로써 해당 클로저가 탈출이 가능한 클로저임을 명시합니다.
typealias VoidVoidClosure = () -> Void //타입을 임의로 정해줍니다
let firstClosure: VoidVoidClosure = {
print("Closure A")
}
let secondClosure: VoidVoidClosure = {
print("Closure B")
}
//first와 second 매개변수 클로저는 함수의 반환 값으로 사용될 수 있으므로 탈출 클로저입니다.
func returnOneClosure(first: @escaping VoidVoidClosure, second: @escaping
VoidVoidClosure, shouldReturnFirstClosure: Bool) -> VoidVoidClosure {
return shouldReturnFirstClosure ? first : second //클로저를 반환합니다
}
// 함수에서 반환한 클로저가 함수 외부의 상수에 저장 되었습니다.
let returnedClosure: VoidVoidClosure = returnOneClosure(first: firstClosure,
second: secondClosure, shouldReturnFirstClosure: true)
returnedClosure()
var closures: [VoidVoidClosure] = []
// closure 매개변수 클로저는 함수 외부의 변수에 저장될 수 있으므로 탈출 클로저입니다
func appendClosure(closure: @escaping VoidVoidClosure){
//전달인자로 전달받은 클로저가 함수 외부의 변수 내부에 저장되므로 함수를 탈출합니다
closures.append(closure)
}
위에서 first와 second는 returnOneClosure의 반환 값으로 사용되지만 returnOneClosure 함수가 종료될 때까지 실행이 되지 않아 탈출 클로저라는 것을 알 수 있습니다. 따라서 @escaping 키워드를 붙여주었는데 이 경우 @escaping 키워드가 없으면 코드는 에러가 나게 됩니다.
그리고 클로저는 클로저 내부에서 타입 내부의 프로퍼티나 메서드, 서브스크립트 등에 접근할 때 self 키워드를 써주어야 합니다.
간단히 말하면 클로저 내부에서 클로저를 포함한 클래스나 함수의 변수, 상수 등을 사용할 때 ex) self.변수 형식으로 사용해주어야 한다는 것입니다. 비탈출 클로저에서의 self는 선택사항입니다.
6. withoutActuallyEscaping
프로그래밍 중에는 전달한 비탈출 클로저가 탈출 클로저인 척 해야하는 경우가 있습니다.
func hasElements(in array: [Int], match prediction: (Int) -> Bool) -> Bool {
return (array.lazy.filter { predicate($0) }.isEmpty == false)
}
위 hasElements 함수의 match 매개변수는 뒤에 @escaping 키워드가 없으므로 탈출 클로저가 아닙니다.
하지만 그 아래 사용된 filter 메서드는 lazy로 설정되어 있고 lazy 컬렉션은 비동기 작업 시 사용되므로 filter 메서드는 탈출 클로저를 전달해주어야 합니다. 우선 위의 코드는 에러를 유발하는 코드겠죠.
이런 경우는 아래와 같은 방법을 택해주면 됩니다.
let numbers: [Int] = [2, 4, 6, 8]
let evenNumberPredicate = { (number: Int) -> Bool in
return number % 2 == 0
}
let oddNumberPredicate = { (number: Int) -> Bool in
return number % 2 == 1
}
func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
return withoutActuallyEscaping(predicate, do: { escapablePredicate in
return (array.lazy.filter {escapablePredicate($0)}.inEmpty == false)
})
} // withoutActuallyEscaping 함수는 predicate를 매개변수로 받음
let hasEvenNumber = hasElements(in: numbers, match: evenNumberPredicate)
let hasOddNumber = hasElements(in: numbers, match: oddNumberPredicate)
print(hasEvenNumber) // true
print(hasOddNumber) // false
위의 예시에서는 withoutActuallyEscaping 함수는 predicate 를 매개변수로 전달받았고 이를 실행할 탈출 클로저로 전달해주었습니다.
이처럼 withoutActuallyEscaping 함수를 사용하면 비탈출 클로저를 받아 클로저 함수로 전달해줌으로써 에러를 방지해줄 수 있습니다.
해당 글은 야곰님의 스위프트 프로그래밍 3판 을 기반으로 한 정리 글이며 문제가 있을 시 삭제하도록 하겠습니다.
스위프트 프로그래밍 3판 eBook 구매 링크: www.yes24.com/Product/Goods/81530016
야곰님의 블로그: blog.yagom.net/
'Swift > 책 정리' 카테고리의 다른 글
[스위프트 프로그래밍 3판] - 15. 맵, 필터, 리듀스 (0) | 2021.03.18 |
---|---|
[스위프트 프로그래밍 3판] - 14. 옵셔널 체이닝과 빠른 종료 (1) | 2021.03.17 |
[스위프트 프로그래밍 3판] - 12. 접근제어 (0) | 2021.03.10 |
[스위프트 프로그래밍 3판] - 11. 인스턴스 생성 및 소멸 (0) | 2021.03.09 |
[스위프트 프로그래밍 3판] - 10. 프로퍼티와 메서드 (0) | 2021.03.08 |