지난 시간에 개인적으로 클로저에 대해서 좀 더 알아봤는데 오늘 강의에서는 클로저에 대한 진도를 나갔다.
복습 차원에서 강의를 들으면서 다시 한 번 정리해봤다.
클로저
클로저는 기능을 갖고 있는 코드 블록이다.
클로저에서는 상수와 변수에 대한 참조를 캡쳐하고 저장할 수 있으며 크게 3가지로 구분한다.
- 전역 함수
- 중첩 함수
- 클로저 표현식
1. 전역 함수
첫 번째로 전역함수는 우리가 프로그래밍을 하면서 정의하고 호출하는 함수들이다. func 키워드를 통해 정의한다. 전역함수는 이름을 가지고 있고 어떠한 값도 캡쳐하지 않는 클로저이다.
func justFunction() -> String {
return "KODO"
}
2. 중첩 함수
두 번째로 중첩 함수는 이름을 가지고 둘러싼 함수로부터 값을 캡쳐할 수 있는 클로저이다.
func outerFunction() -> (() -> Int) {
func innerFunction() -> Int {
var num: Int = 1
return num
}
return innerFunction
}
3. 클로저 표현식
in 키워드를 기준으로 클로저 헤더와 클로저 바디로 구분된다.
let closure = { (name: String) -> String in
return "\(name)"
}
클로저 표현식으로 변환해보기
공식 문서에 나와있는 것처럼 sorted 메서드를 가지고 클로저에 대해 알아보도록 하자.
[String] 타입의 배열을 내림차순으로 정렬하기 위해서는 sorted 메서드에 값에 대한 비교를 할 수 있는 함수를 전달 인자로 전달해야 한다.
let alphabets = ["a", "b", "c", "d", "e"]
func comparedTwoValues(_ s1: String, _ s2: String) -> Bool {
return s1 > s2 // 내림차순으로 정렬
}
let reversed = alphabets.sorted(by: compareTwoValues)
print(reversed)
지금 같은 경우는 간단하게 두 수를 비교하는 상황이라서 값을 비교할 수 있는 함수를 정의하고 이를 전달인자로 넘겨줬지만 매번 함수를 정의한다는 것은 꽤나 귀찮을 것이다.
이러한 문제를 해결해주는 것이 바로 클로저이다.
우선 클로저는 아래와 같은 형태를 띄며, in 키워드를 기준으로 클로저 헤드와 클로저 바디로 나뉜다.
{ (파라미터) -> 반환 타입 in
실행 코드
}
우선 위의 코드를 클로저로 변환해보면 아래와 같다. 앞서 말한 것처럼 in 키워드를 통해서 클로저 헤더와 클로저 바디를 구분한다.
let alphabets = ["a", "b", "c", "d", "e"]
/*
func comparedTwoValues(_ s1: String, _ s2: String) -> Bool {
return s1 > s2 // 내림차순으로 정렬
}
let reversed = alphabets.sorted(by: compareTwoValues)
*/
let reversed = alphabets.sorted(by: {(s1: String, s2: String) -> Bool in
return s1 > s2
})
print(reversed)
코드가 조금 간단해지긴 했는데 눈에 띄는 변화를 느끼지는 못하겠다.
1. 더 줄여보기
클로저는 전달 인자로 전달되기에 Swift가 파라미터 타입과 반환 되는 값을 유추할 수 있다.
alphabets 상수가 [String] 타입이므로 전달 인자 역시, (String, String) → Bool 인 것을 유추할 수 있게 된다.
결과적으로 파라미터 타입과 반환 타입을 생략할 수 있다.
let reversed = alphabets.sorted(by: { s1, s2 in return s1 > s2 })
2. 더 더 줄여보기
한 줄로 표현 가능한 클로저는 심지어 return도 생략 가능하다.
let reversed = alphabets.sorted(by: { s1, s2 in s1 > s2 })
/* 아래 코드는 컴파일 Error
let reversed = alphabets.sorted(by: { s1, s2 in
s1 > s2
print(s1)
})
*/
3. 더 더 더 줄여보기
위 코드는 클로저 표현식이 전체 바디이므로 in 키워드조차 생략할 수 있다.
let reversed = alphabets.sorted(by: { $0 > $1 })
4. 더 더 더 더 줄여보기
let reversed = alphabets.sorted(by: >)
후행 클로저
함수의 마지막 인자에 클로저 표현식을 전달해야 하고, 클로저 표현식이 긴 경우 후행 클로저로 작성하는 것이 유용하다.
이러한 후행 클로저는 첫 번째 클로저의 Argument Label을 작성하지 않아도 된다.
func someFunctionThatTakesAClosure(closure: () -> Void) {
// function body goes here
}
// 일반적인 클로저 표현식으로 작성
someFunctionThatTakesAClosure(closure: {
// closure's body goes here
})
// 후행 클로저로 작성
someFunctionThatTakesAClosure() {
// trailing closure's body goes here
}
또 다른 예시를 보자. 클로저를 통해서 오류를 처리하는 코드를 명확하게 분리할 수 있다.
func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
if let picture = download("photo.jpg", from: server) {
completion(picture)
} else {
onFailure()
}
}
// 첫 번째 클로저의 Argument Label인 completion을 생략
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: { // 두 번째 클로저의 Argument Label은 그대로 작성
print("Couldn't download the next picture.")
}
캡쳐값 (클로저는 참조 타입)
클로저는 정의된 코드 블럭 내에서 상수와 변수를 캡쳐할 수 있다.
그래서 상수와 변수를 정의했던 범위(스코프)를 벗어나더라도 클로저에서 해당 상수와 변수의 값을 참조하고 수정할 수 있다.
말이 좀 어렵지만 쉽게 설명하자면 상수와 변수의 상태를 가지고 있을 수 있다는 말이다.
실제 코드를 보면서 천천히 살펴보자.
func makeIncrementer(forIncrement amount: Int) -> (() -> Int) {
var runningTotal = 0
func incrementer() -> Int {
runningTotal += amount
return runningTotal
}
return incrementer
}
makeIncrementer 함수는 클로저를 리턴하고 있다. 내부에 runningTotal 변수가 정의되어 있고 중첩 함수인 incrementer 함수에서는 runningTotal에 전달인자로 받은 amount만큼 더 해준다.
이제 이 함수를 incrementByTen 상수에 할당하고 호출해보자.
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementByTen()
print(incrementByTen())
어떤 값이 출력될까?
정답은 30이다.
10 + 10 + 10 (print로 출력되기 전에 한번 더 호출)
중첩 함수인 incrementer 함수는 매개변수로 runningTotal을 직접적으로 전달 받지는 않지만 runningTotal과 amount를 참조를 캡쳐하고 이를 함수 내에서 사용하고 있다.
그 결과 함수가 종료되더라도 이전 호출 결과가 사라지지 않고 다음에 호출될 때 이 값을 사용할 수 있는 것이다.
그렇다면 아래의 코드의 결과는 어떤 값이 출력될까?
let incrementByTen = makeIncrementer(forIncrement: 10)
incrementByTen()
incrementByTen()
print(incrementByTen()) // 30
let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
alsoIncrementByTen()
print(incrementByTen())
정답은 60이다.
앞서 말했던 것처럼 클로저는 내부의 상수와 변수에 대한 참조를 캡쳐하고 있다.
alsoIncrementByTen 역시 같은 클로저를 참조하고 있기에 어떤 상수를 호출하던 같은 클로저가 실행되는 것이다.
이걸 통해 클로저는 참조 타입이라는 것을 알 수 있다.
이스케이프 클로저
함수에 인자로 클로저를 전달하고 함수 밖의 변수에 클로저를 할당하거나 함수가 종료된 뒤에 실행되는 클로저를 이스케이핑 클로저라고 한다.
우선 클로저가 아닌 일반적인 코드를 기준으로 살펴보자.
class Test {
var num: Int? // 전역
func setNumber(number: Int) {
self.num = number
}
}
이처럼 함수 내부에서 전달인자를 전역 변수인 num에 할당하는 것은 아무런 문제가 되지 않는다.
그렇다면 아래 코드는 어떨까?
class Test {
var closure: (() -> Void)?
func setClosure(_ param: () -> Void) {
self.closure = param
}
}
non-escaping parameter를 escaping closure에 할당할 수 없다는 오류가 발생한다.
그렇다면 escaping 어노테이션이 필요한 이유는 무엇일까?
앞서 말했던 것처럼 클로저는 참조 타입이고 함수 내부에서 값이 할당됐을 때 실행하는 것이 아니라 외부에서 호출됐을 때 실행하게 된다.
즉, 클로저가 전달되는 함수보다 더 오래 지속되어야 하는데 이를 도와주는 것이 escaping 어노테이션이다.
class Test {
var closure: (() -> Void)?
func setClosure(_ param: @escaping () -> Void) {
self.closure = param
}
}
escaping 어노테이션을 추가하면 정상적으로 컴파일이 된다.
🔗 참고한 것
클로저 (Closures) - Swift
컨텍스트로 타입 유추 (Inferring Type From Context)
bbiguduk.gitbook.io
👀 회고
오늘 "함께 자라기" 스터디를 진행하면서 실행 프레임과 학습 프레임에 대해 알게 됐다. 두 군집에 대한 실험과 서로 상반된 자세의 두 명의 개발자를 보면서 나 역시, '자라기' 보다는 '잘하기'에 초점을 맞춰서 지내왔다. 가장 최근에 임했던 NC 인턴에서도 제한된 기간 안에 과제를 수행해야 한다는 압박감에 '잘하기'만 생각하고 '자라기'는 스스로 핑계를 대며 다소 멀리한 경향이 있었다. "실행 프레임과 학습 프레임" 파트를 읽으면서 진정한 성장이 무엇인가에 대해 다시 한 번 생각하는 계기가 되었다.
'💻 개발 > iOS' 카테고리의 다른 글
[iOS / SwiftUI] 키보드가 사라지지 않아요...😩 (0) | 2022.10.21 |
---|---|
[iOS / SwiftUI] ForEach로 View를 리펙토링 해볼까요? (0) | 2022.10.11 |
[iOS / Swift] 클로저와 프로퍼티, 가볍게 알아보기 (0) | 2022.10.02 |
[iOS / Swift] 상속 VS 익스텐션 (0) | 2022.09.27 |
[TIL] 22.08.25 (0) | 2022.08.25 |