클로저

피터·2022년 8월 6일
0
post-thumbnail

클로저는 변수나 상수가 선언된 위치에서 참조(Referenc)를 획득(Capture)하고 저장할 수 있습니다. 이를 변수나 상수의 클로징이라고 하며 클로저는 여기서 착안된 이름입니다.

클로저의 몇 가지 모양 중 하나가 함수입니다.

클로저는 세 가지 형태가 있습니다.

  • 이름이 있으면서 어떤 값도 획득하지 않는 전역함수의 형태
  • 이름이 있으면서 다른 함수 내부의 값을 획득할 수 있는 중첩된 함수의 형태
  • 이름이 없고 주변 문맥에 따라 값을 획득할 수 있는 축약 문법으로 작성한 형태

클로저로 표현할 수 있는 것

  • 클로저는 매개변수와 반환 값의 타입을 문맥을 통해 유추할 수 있기 때문에 매개변수와 반환 값의 타입을 생략가능
  • 클로저에 단 한 줄의 표현만 들어있다면 암시적으로 이를 반환 값으로 취급
  • 축약된 전달인지 이름을 사용할 수 있음
  • 후행 클로저 문법을 사용할 수 있음

기본 클로저

let names: [String] = ["채드", "히로", "피터", "토니", "챈", "리버"]

func backwards(first: String, second: String) -> Bool {
    print("\(first) \(second) 비교 중")
    return first > second
}

let reversed: [String] = names.sorted(by: backwards(first:second:))
print(reversed)

// ["히로", "피터", "토니", "챈", "채드", "리버"]

클로저 표현은 통상 아래 형식으로 따릅니다.

{ (매개변수들) -> 반환 타입 in 
	실행 코드
}

위를 사용해서 backwards 메서드를 클로저 형태로 바꿔보겠습니다. 클로저에서도 함수 처럼 매개변수 이름을 지정할 수 있는데 대신, 기본값을 사용할 수 없습니다.

let reversed: [String] = names.sorted(by: { (first: String, second: String) -> Bool in
    return first > second
})
print(reversed)

후행 클로저

좀 더 클로저를 읽기 쉽게 바꾸어봅시다. 함수나 메서드의 마지막 전달인자로 위치하는 클로저는 함수나 메서드의 소괄호를 닫은 후에 작성해도 됩니다. 가독성이 조금 떨어진다 싶으면 후행 클로저 기능을 사용하면 좋습니다.

단, 후행 클로저는 맨 마지막 전달인자로 전달되는 클로저에만 해당되므로 전달인자로 클로저 여러 개를 전달할 때는 맨 마지막 클로저만 후행 클로저로 사용할 수 있습니다.

매개변수에 클로저가 여러 개 있는 경우 다중 후행 클로저 문법을 사용할 수 있습니다. 첫 번째 클로저의 전달인자 레이블은 생략합니다.

let reversed: [String] = names.sorted() { (first: String, second: String) -> Bool in
    print("\(first) \(second) 비교 중")
    return first > second
}

// 소괄호도 생략 가능 
let reversed: [String] = names.sorted { (first: String, second: String) -> Bool in
    print("\(first) \(second) 비교 중")
    return first > second
}

func doMultiTrailingClosure(do: String,
                            onSuccess: () -> Void,
                            onFailure: (String) -> Void) {
    
}

doMultiTrailingClosure(do: "해라") {
    // onSuccess의 경우
} onFailure: { <#String#> in
    // onFailure의 경우 
}

클로저 표현 간소화

문맥을 이용한 타입 유추

전달인자로 전달하는 클로저를 구현할 때는 매개변수의 타입이나 반환 값의 타입을 표현해주지 않고 생략하더라도 타입 유추를 지원해줍니다.

let reversed: [String] = names.sorted { (first, second) -> Bool in
    print("\(first) \(second) 비교 중")
    return first > second
}

단축 인자 이름

매개변수의 이름도 생각할 수 있고 이를 대신해서 단축 인자 이름이 있으며 첫 번재 전달인자부터 $0, $1, $2, $3 … 순서로 $와 숫자의 조합으로 표현합니다. 단축 인자 표현을 사용하게 되면 매개변수 및 반환 타입과 실행 코드를 구분하기 위해 사용했던 키워드 in을 사용할 필요도 없어집니다.

let reversed: [String] = names.sorted {
    return $0 > $1
}

암시적 표현

return 키워드 마저 생략해봅시다! 생략 가능합니다!

let reversed: [String] = names.sorted { $0 > $1 }

연산자 함수

비교 연산자는 두 개의 피연산자를 통해 Bool 타입을 반환을 줍니다. sorted(by:) 메서드에 전달한 클로저와 동일한 조건입니다. 클로저는 매개변수의 타입과 반환 타입이 연산자를 구현한 함수의 모양과 동일하다면 연산자만 표기하더라도 알아서 연산하고 반환합니다.

let reversed: [String] = names.sorted(by: >)

값 획득

클로저는 자신이 정의된 위치의 주변 문맥을 통해 상수나 변수를 획득(Capture)할 수 있습니다. 값 획득을 통해 클로저는 주변에 정의한 상수나 변수가 더 이상 존재하지 않더라도 해당 상수나 변수의 값을 자신 내부에서 참조하거나 수정할 수 있습니다.

클로저는 주로 비동기 처리에서 많이 사용됩니다. 이런 이유 때문에 클로저의 값 획득에 대해서 알고 있어야 합니다.

클로저를 통해 비동기 콜백을 작성하는 경우, 현재 상태를 미리 획득해두지 않으면, 실제로 클로저의 기능을 실행하는 순간에는 주변의 상수나 변수가 이미 메모리에 존재하지 않은 경우가 발생합니다.

중첩 함수도 하나의 클로저의 형태이기 때문에 다음과 같은 예시를 들어보겠습니다.

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

let incrementByTwo: (() -> Int) = makeIncrementer(forIncrementer: 2)

let first: Int = incrementByTwo() // 2
let second: Int = incrementByTwo() // 4
let third: Int = incrementByTwo() // 6

위의 코드를 보면 incrementer() 함수는 runningTotal과 amount 두 변수의 참조를 획득할 수 있습니다. 참조를 획득하면 runningTotal과 amount는 makeIncrementer 함수의 실행이 끝나도 사라지지 않습니다. 따라서 makeIncrementer(forIncrementer:) 함수를 사용하여 incrementByTwo라는 이름의 상수에 increment 함수를 할당해줬습니다. 그렇기 때문에 incrementByTwo를 호출할 때마다 runningTotal은 값이 2씩 증가합니다.

💡 그렇다면 runningTotal과 amount는 어디에 저장되어 있을까?
클로저 생성과 유사하게 힙메모리에 저장되어 있음

클로저는 참조 타입

함수나 클로저를 상수나 변수에 할당할 때마다 사실은 상수나 변수에 함수나 클로저의 참조를 설정하는 것입니다. 즉, 상수나 변수에 함수나 클로저를 할당하는 것은 함수나 클로저의 주소값을 할당하는 것입니다.

탈출 클로저

함수의 전달인자로 전달한 클로저가 함수 종료 후에 호출될 때 클로저가 함수를 탈출한다고 표현합니다. 클로저를 매개변수로 갖는 함수를 선언할 때 매개변수 이름의 콜론 뒤에 @escaping 키워드를 사용하여 클로저가 탈출하는 것을 허용한다고 명시해줄 수 있습니다.

예를 들어 비동기 작업을 실행하는 함수들은 클로저를 컴플리션 핸들러 전달인자로 받아옵니다. 비동기 작업으로 함수가 종료되고 난 후 호출할 필요가 있는 클로저를 사용해야할 때 탈출 클로저가 필요합니다.

함수가 전달된 클로저가 함수의 동작이 끝난 후 사용할 필요가 없을 때 비탈출 클로저를 사용합니다.

var completionHandler: (() -> Void)?
func someFunctionWithEscapingClosure(completion: @escaping () -> Void) {
    completionHandler = completion
}

someFunctionWithEscapingClosure {
    print("이거 지금 함수 끝났는데 사용되고 있어요")
}

completionHandler?() // 이거 지금 함수 끝났는데 사용되고 있어요

만일 클래스 인스턴스 안에서 클로저를 사용할 경우 비탈출 클로저에서는 인스턴스의 프로퍼티를 사용하기 위해 self 키워드를 생략해도 무관하지만, 탈출 클로저에서는 값 획득을 위한 self 키워드를 사용하여 프로퍼티에 접근해야만 합니다.

withoutActuallyEscaping

비탈출 클로저로 전달한 클로저가 탈출 클로저인 척 해야하는 경우가 있습니다. 실제로는 탈출하지 않는데 다른 함수에서 탈출 클로저를 요구하는 상황에 해당합니다.

func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
    return array.lazy.filter { predicate($0) }.isEmpty == false
}
// predicate가 비탈출 클로저라서 오류 메세지를 받게 됩니다. 

위의 코드에서 predicate는 비탈출 클로저일 필요는 없습니다. 그래서 해당 클로저가 탈출 클로저인양 사용할 수 있게 돕는 withoutActuallyEscaping(_:do:) 함수가 있습니다.

func hasElements(in array: [Int], match predicate: (Int) -> Bool) -> Bool {
    return withoutActuallyEscaping( predicate ) { escapingClosure in
        return array.lazy.filter { escapingClosure($0) }.isEmpty == false
    }
}

자동 클로저

함수의 전달인자로 전달하는 표현을 자동으로 변환해주는 클로저를 자동 클로저라고 합니다. 자동 클로저는 전달인자를 갖지 않습니다. 자동 클로저는 호출되었을 때 자신이 감사고 있는 코드의 결과값을 반환합니다. 자동 클로저는 함수로 전달하는 클로저를 (소괄호와 중괄호를 겹쳐서 써야 하는) 어려운 클로저 문법을 사용하지 않고도 클로저로 사용할 수 있도록 문법적 편의를 제공합니다.

하지만 직접 자동 클로저를 호출하는 함수를 구현하는 일은 흔치 않을 겁니다.

var 대기명단 = ["호철", "명희", "영숙", "기백"]
print(대기명단.count) // 4

let 들어오세요: () -> String = {
    return 대기명단.removeFirst()
}
print(대기명단.count) // 4

print("\(들어오세요())님 지금 들어오세요!") // 호철님 지금 들어오세요!

지금 까지 봤던 일반적인 매개변수로 클로저를 넣는 방식을 보여드리겠습니다.

var 대기명단 = ["호철", "명희", "영숙", "기백"]

func 다음고객님들어오세요(_ customerProvider: () -> String) {
    print("\(customerProvider())님 들어오세요")
}

다음고객님들어오세요 { 대기명단.removeFirst() } // 호철님 들어오세요

다음은 @autoclosure 속성을 주어 자동 클로저 기능을 하는 코드 입니다.

var 대기명단 = ["호철", "명희", "영숙", "기백"]

func 다음고객님들어오세요(_ customerProvider: @autoclosure () -> String) {
    print("\(customerProvider()) 님 들어오세요")
}

다음고객님들어오세요(대기명단.removeFirst()) // 이 부분이 위와 다릅니다.

기존의 ‘다음고객님들어오세요’ 함수와 동일한 역할을 하지만 매개변수에 @autoclosure 속성을 주었기 때문에 자동 클로저 기능을 사용합니다. 자동 클로저 속성을 부여한 매개변수는 클로저 대신에 ‘대기명단.removeFirst()’ 코드의 실행 결과인 String 타입의 문자열을 전달인자로 받게 됩니다. String 타입의 값이 자동 클로저 매개변수에 전달되면 String 값을 매개변수가 없는 String 값을 반환하는 클로저로 반환해줍니다.

String 타입의 값을 전달 받는 이유는 자동 클로저의 반환 타입이 String이기 때문입니다.

이렇게 String 값으로 전달된 전달인지가 자동으로 클로저로 반환되기 때문에 자동 클로저라고 부릅니다.

💡 자동 클로저의 과도한 사용
자동 클로저를 과도하게 사용하면 이해하기 어렵게 만들 가능성이 아주 큽니다. 자동 클로저를 사용하고자 한다면 함수 이름 또는 매개변수 이름 등은 자동 클로저를 사용한다는 명확한 의미를 전달할 수 있는 이름으로 명명하는 것이 좋습니다.

자료 출처: 야곰 스위프트 프로그래밍 3판

"이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다."
profile
iOS 개발자입니다.

0개의 댓글