[Swift 공식문서 읽기]Closures

llim🧚🏻‍♀️·2021년 8월 11일
1

Swift

목록 보기
7/26
post-thumbnail

안녕하세요. 엘림입니다🙇🏻‍♀️

Swift 공식 문서 정독하기 7편입니다!

제 스타일대로 정리했으니 추가적으로 더 필요한 정보는
공식문서 링크를 눌러 확인해주세용!

좀 더 편하게 보기위해 한국어로 번역된 사이트를 함께 확인했습니다!ㅎㅎ

자, 그럼 시작해볼까요

이 글은 공부하면서 작성한 글이기 때문에 잘못된 정보가 있을 수 있습니다.🥺
금방 잊어버릴... 미래의 저에게 다시 알려주기 위한 글이다보니
혹시라도 틀린 부분이 있다면, 댓글로 친절하게 알려주시길 부탁드립니다.🙏


클로저

클로저는 코드에서 전달되고 사용할 수 있는 기능이 자체적으로 포함되어있는 블록입니다. 스위프트에서 클로저는 C와 Objective-C, 그리고 다른 프로그래밍 언어에서 람다와 비슷한 블록입니다.

클로저는 정의된 컨텍스트에서 모든 상수 및 변수에 대한 참조를 캡쳐하고 저장할 수 있습니다. 스위프트는 캡쳐와 관련한 모든 메모리를 알아서 처리합니다. (캡쳐에 대한 설명은 아래에서 계속 나옵니다.)

클로저는 다음 세 가지 형태 중 하나를 취합니다.(함수도 클로저 입니다.)

  • 전역 함수: 이름이 있고, 값을 캡쳐하지 않는 클로저입니다.
  • 중첩 함수: 이름이 있고, 둘러싸는 함수에서 값을 캡처할 수 있는 클로저입니다.
  • 클로저: 이름이 없고, 주변 컨텍스트에서 값을 챕쳐할 수 있는 경량구문으로 작성된 클로저입니다.

클로저 표현식은 일반적으로 간단하고, 깔끔한 구문을 장려하는 최적화와로 인해, 깔끔하고 명확한 스타일을 가지고 있습니다. 이러한 최적화에는 다음이 포함됩니다.

  • 문맥(context)에서 매개 변수 및 반환 값 유형 추론
  • 단일 표현식 클로저에서의 암시적 반환
  • 약식화된 인수 이름
  • 후행 클로저 구문

클로저 표현

클로저 표현은 인라인 클로저(함수로 따로 정의된 형태가 아닌, 인자로 들어가 있는 형태) 를 명확하게 표현하는 방법으로 문법에 초첨이 맞춰져 있습니다. 클로저 표현은 코드의 명확성과 의도를 잃지 않으면서도 문법을 축약해 사용할 수 있는 다양한 문법의 최적화 방법을 제공합니다.

정렬 메소드

표준 라이브러리에 sorted(by:)라는 메소드가 있습니다. 이 by에 어떤 방법으로 정렬을 수행할 것인지에 대한 클루저를 넣으면, 그 방법대로 정렬된 배열을 얻을 수 있습니다.

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]

backward 클로저를 만들고 그것을 names.sorted(by: backward)에 넣으면 원본 배열의 순서가 바뀐 배열을 정렬 결과로 얻을 수 있습니다.

클로저 표현 문법

{ (parameters) -> return type in
    statements
}

클로저 표현 문법은 인자로 넣을 parameters, 인자 값으로 처리할 내용을 기술하는 statements 그리고 return type의 형태를 띱니다. 앞의 backward클로저를 이용해 배열을 정렬하는 코드는 클로저 표현을 이용해 다음과 같이 바꿀 수 있습니다.

reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
    return s1 > s2
})

이렇게 인자로 들어가있는 인라인 클로저의 몸통은 in 키워드 다음에 시작합니다. 사용할 매개변수(인자, parameter)와 반환 타임을 알았으니 이제 그것들으 적절히 처리해 넘겨줄 수 있다는 뜻이죠.

문맥에서 타입 추론

사실 문맥에서 이미 인자의 어떤 타입의 인자가들어와야 하는지 알기 때문에, 인자의 타입을 생략할 수 있습니다.(만약 가독성과 코드의 모호성을 피하고 싶다면, 당연히 명시할 수도 있습니다.)

reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

단일 표현 클로저에서의 암시적 반환

reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )

단일 표현 클로저에서는 반환 키워드를 생략할 수 있습니다.
이렇게 표현해도 어떠한 모호성도 없습니다. s1과 s2를 인자로 받아 그 두 값을 비교한 결과를 반환합니다.

인자 이름 축약

Swift는 인라인 클로저에 자동으로 축약 인자 이름을 제공합니다. 이 인 자를 사용하면 인자 값을 순서대로 $0, $1, $2 등으로 사용할 수 있습니다. 축약 인자 이름을 사용하면 인자 값과 그 인자로 처리할 때 사용하는 인자가 같다는 것을 알기 때문에 인자를 입력 받는 부분과 in 키워드 부분을 생략 할 수 있습니다. 그러면 이런 형태로 축약 가능합니다.

reversedNames = names.sorted(by: { $0 > $1 } )

축약은 되었지만 논리를 표현하는데는 지장이 없습니다. 인라인 클로저에 생략된 내용을 포함해 설명하면 1. $0과 $1 인자를 두개 받아서 2. $0이 $1 보다 큰지를 비교하고 3. 그 결과(Bool)를 반환해라. 입니다.

연산자 메소드

축약이 이게 끝인줄 아셨겠지만 아닙니다. 😉 여기서 더 줄일 수 있습니다. Swift의 String 타입 연산자에는 String끼리 비교할 수 있는 비교 연산자(>) 를 구현해 두었습니다. 이 때문에 그냥 이 연산자를 사용하면 됩니다.

reversedNames = names.sorted(by: >)

후위 클로저

만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있습니다. 이런 형태의 함수와 클로저가 있다면

func someFunctionThatTakesAClosure(closure: () -> Void) {
    // function body goes here
}

위 클로저의 인자 값 입력 부분과 반환 형 부분을 생략해 다음과 같이 표현할 수 있고

someFunctionThatTakesAClosure(closure: {
    // closure's body goes here
})

이것을 후위 클로저로 표현하면 아래와 같이 표현할 수 있습니다. 함수를 대괄호 ( {, } )로 묶어 그 안에 처리할 내용을 적으면 됩니다. 모르고 사용하셨다면 이런 일반적인 전역함수 형태가 사실 클로저를 사용하고 있던 것이었습니다. 😉

someFunctionThatTakesAClosure() {
    // trailing closure's body goes here
}

앞의 정렬 예제를 후위 클로저를 이용해 표현하면 이렇게 표현할 수 있습니다.

reversedNames = names.sorted() { $0 > $1 }

만약 함수의 마지막 인자가 클로저이고 후위 클로저를 사용하면 괄호()를 생략할 수 있습니다.

reversedNames = names.sorted { $0 > $1 }

이번에는 후위 클로저를 이용해 숫자(Int)를 문자(String)로 매핑(Mapping)하는 예제를 살펴 보겠습니다. 다음과 같은 문자와 숫자가 있습니다.

let digitNames = [
    0: "Zero", 1: "One", 2: "Two",   3: "Three", 4: "Four",
    5: "Five", 6: "Six", 7: "Seven", 8: "Eight", 9: "Nine"
]
let numbers = [16, 58, 510]

let strings = numbers.map { (number) -> String in
    var number = number
    var output = ""
    repeat {
        output = digitNames[number % 10]! + output
        number /= 10
    } while number > 0
    return output
}
// let strings는 타입 추론에 의해 문자 배열([String])타입을 갖습니다.
// 결과는 숫자가 문자로 바뀐 ["OneSix", "FiveEight", "FiveOneZero"]가 됩니다.

위 코드는 각 자리수를 구해서 그 자리수를 문자로 변환하고, 10으로 나눠서 자리수를 바꾸며 문자로 변환하는 것을 반복합니다. 이 과정을 통해 숫자 배열을, 문자 배열로 바꿀 수 있습니다. number값은 상수인데, 이 상수 값을 클로저 안에서 변수 var로 재정의 했기 때문에 number값의 변환이 가능합니다. 기본적으로 함수와 클로저에 넘겨지는 인자 값은 상수입니다

func loadPicture(from server: Server, completion: (Picture) -> Void, onFailure: () -> Void) {
    if let picture = download("photo.jpg", from: server) {
        completion(picture)
    } else {
        onFailure()
    }
}

loadPicture(from: someServer) { picture in
    someView.currentPicture = picture
} onFailure: {
    print("Couldn't download the next picture.")
}

첫 번째 클로저는 성공적인 다운로드 후 그림을 표시하는 완료 핸들러입니다. 두 번째 클로저는 사용자에게 오류를 표시하는 오류 처리기입니다.
이 방법으로 함수를 작성하면 두 가지 상황을 모두 처리하는 하나의 클로저를 사용하는 대신 성공적인 다운로드 후 사용자 인터페이스를 업데이트하는 코드에서 네트워크 오류 처리를 담당하는 코드를 명확하게 분리할 수 있습니다.

값 캡쳐

클로저는 정의된 주변의 문맥에서 상수와 변수를 캡쳐할 수 있습니다. 그러면 클로저는 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도(원본 값이 사라져도) body 안에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다.

스위프트에서 값을 캡쳐할 수 있는 가장 간단한 형태의 클로저는, 다른 함수의 본문 내에서 작성된 중첩 함수입니다. 중첩 함수는 외부 함수의 인수를 캡처할 수 있으며, 외부 함수 내에 정의된 모든 상수 및 변수를 캡처할 수도 있습니다.

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

중첩 된 incrementer() 함수는 주변 컨텍스트에서 runningTotalamount의 두 값을 캡처합니다. 이러한 값을 캡처 한 후에는 호출 될 때마다 runningTotal을 양만큼 증가시키는 클로저로 incrementermakeIncrementer에 의해 반환됩니다.(makeIncrementer의 반환 타입은 ()-> Int입니다. 반환하는 함수에는 매개 변수가 없으며 호출 될 때마다 Int 값을 반환합니다.)

단독으로 중첩 된 incrementer() 함수를 보면 비정상적으로 보일 수 있습니다.

func incrementer() -> Int {
    runningTotal += amount
    return runningTotal
}

incrementer() 함수에는 매개 변수가 없지만 함수 본문 내에서 runningTotal 및 양을 참조합니다. 주변 함수에서 runningTotalamount에 대한 참조를 캡처하고 자체 함수 본문 내에서 사용하여, 이를 수행합니다. 참조로 캡처하면 makeIncrementer 호출이 종료 될 때 runningTotalamount가 사라지지 않고 다음에 incrementer 함수가 호출 될 때 runningTotal을 사용할 수 있습니다.

최적화로서 스위프트는 값이 클로저에 의해 변경되지 않고, 클로저가 생성 된 후 값이 변경되지 않은 경우 값의 사본을 캡처하고 저장할 수 있습니다.
또한 Swift는 더 이상 필요하지 않을 때 변수를 처리하는 것과 관련된 모든 메모리 관리를 처리합니다.

let incrementByTen = makeIncrementer(forIncrement: 10)

incrementByTen()
// returns a value of 10
incrementByTen()
// returns a value of 20
incrementByTen()
// returns a value of 30

이 예제에서는 호출 될 때마다 runningTotal 변수에 10을 더하는 증분 함수를 참조하도록 incrementByTen이라는 상수를 설정합니다. 함수를 여러 번 호출하면이 동작이 실제로 표시됩니다.

두 번째 상수를 만들면 별도의 새 runningTotal 변수에 대한 자체 저장된 참조가 있습니다.

let incrementBySeven = makeIncrementer(forIncrement: 7)
incrementBySeven()
// returns a value of 7

원래 상수(incrementByTen)를 다시 호출하면 자체 runningTotal 변수가 계속 증가하고 incrementBySeven에 의해 캡처 된 변수에는 영향을주지 않습니다.

incrementByTen()
// returns a value of 40

클래스 인스턴스의 속성에 클로저를 할당하고, 클로저가 인스턴스 또는 해당 멤버를 참조하여 해당 인스턴스를 캡처하면, 클로저와 인스턴스 사이에 강력한 참조주기가 생성됩니다. 스위프트는 캡처 목록을 사용하여 이러한 강력한 참조주기를 깨뜨립니다

클로저는 참조 타입이다

위의 예에서 incrementBySevenincrementByTen은 상수입니다. 그런데 어떻게 runningTotal변수를 계속 증가 시킬 수 있는 걸까요? 이는 함수와 클로저가 참조 타입이기 때문입니다. 함수(또는 클로저)를 상수(또는 변수)에 할당 할 때마다, 해당 상수를 함수에 대한 참조로 할당합니다.

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// returns a value of 50

incrementByTen()
// returns a value of 60

다른 상수(또는 변수)에 클로저를 할당하면 해당 상수가 모두 동일한 클로저를 참조 하여, 클로저의 마지막 상태에서 반영이 됩니다.

Escaping Closures

매개 변수 중 하나로 클로저를받는 함수를 선언 할 때 매개 변수 유형 앞에 @escaping을 작성하여 클로저가 탈출 될 수 있음을 나타낼 수 있습니다.(비동기 작업을 시작하는 많은 함수는 컴플리션 핸들러로 클로저 인수를 사용합니다.)

함수는 작업을 시작한 후 반환되지만, 작업이 완료 될 때까지 클로저가 호출되지 않습니다.(클로저는 나중에 호출하려면 이스케이프해야합니다.)

var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

someFunctionWithEscapingClosure(_ :) 함수는 클로저를 인수로 사용하여 함수 외부에서 선언 된 배열에 추가합니다. 이 함수의 매개 변수를 @escaping으로 표시하지 않으면 컴파일 타임 오류가 발생합니다.

self를 참조하는 이스케이프 클로저는, self가 클래스의 인스턴스를 참조하는 경우 특별히 주의해야합니다. 이스케이프 클로저에서 자신을 캡처하면 실수로 강력한 참조주기를 쉽게 만들 수 있습니다.

일반적으로 클로저는 클로저 본문에서 변수를 사용하여 암시적으로 변수를 캡처하지만, 이런 경우에는 명시적이어야합니다. 즉, @escaping 를 사용하는 클로저에서는 self를 명시적으로 언급해야 합니다.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
    // completionHandlers.append(closure) // 이렇게 사용하면 ERROR
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다.
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

매개변수가 escaping가 아닌 경우, 외부로 꺼낼 수 없습니다.
즉, someFunctionWithNonescapingClosure에서 closure는 completionHandlers에 append가 불가능합니다.

class SomeOtherClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

Autoclosures

자동클로저는 인자 값이 없으며, 특정 표현을 감싸서 다른 함수에 전달 인자로 사용할 수 있는 클로저입니다. 자동클로저는 클로저를 실행하기 전까지 실제 실행이 되지 않습니다. 그래서 계산이 복잡한 연산을 하는데 유용합니다. 왜냐면 실제 계산이 필요할 때 호출되기 때문입니다.

(참고: @autoclosure는 함수의 인자로 전달되는 코드를 감싸서 자동으로 클로저로 만들어 줍니다. 다시말해 일반 표현의 코드를 클로저 표현의 코드로 만들어 주는 역할을 합니다.)

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count)
// Prints "5"

let customerProvider = { customersInLine.remove(at: 0) }
print(customersInLine.count)
// Prints "5"

print("Now serving \(customerProvider())!")
// Prints "Now serving Chris!"
print(customersInLine.count)
// Prints "4"

위 예제 코드를 보면 let customerProvider = { customersInLine.remove(at: 0) } 이 클로저 코드를 지났음에도 불구하고 customersInLine.count 는 변함없이 5인 것을 볼 수 있습니다. 그리고 그 클로저를 실행시킨 print("Now serving (customerProvider())!") 이후에야 배열에서 값이 하나 제거되어 배열의 원소 개수가 4로 줄어든 것을 확인할 수 있습니다. 이렇듯 자동 클로저는 적혀진 라인 순서대로 바로 실행되지 않고, 실제 사용될 때 지연 호출 됩니다.

// customersInLine is ["Alex", "Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: { customersInLine.remove(at: 0) } )
// Prints "Now serving Alex!"

serve함수는 인자로 () -> String) 형, 즉 인자가 없고, String을 반환하는 클로저를 받는 함수 입니다. 그리고 이 함수를 실행할 때는 serve(customer: { customersInLine.remove(at: 0) } )이와 같이 클로저{ customersInLine.remove(at: 0) }를 명시적으로 직접 넣을 수 있습니다.

위 예제에서는 함수의 인자로 클로저를 넣을 때 명시적으로 넣는 경우에 대해 알아 보았습니다. 위 예제를 @autoclosure키워드를 이용해서 보다 간결하게 사용할 수 있습니다.

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
    print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0))
// Prints "Now serving Ewa!"

serve함수의 인자를 받는 부분 customerProvider: @autoclosure () 에서 클로저의 인자()앞에 @autoclosure라는 키워드를 붙였습니다. 이 키워드를 붙임으로써 인자 값은 자동으로 클로저로 변환됩니다. 그래서 함수의 인자 값을 넣을 때 클로저가 아니라 클로저가 반환하는 반환 값과 일치하는 형의 함수를 인자로 넣을 수 있습니다. 그래서 serve(customer: { customersInLine.remove(at: 0) } ) 이런 코드를 @autoclosure키워드를 사용했기 때문에 serve(customer: customersInLine.remove(at: 0)) 이렇게 {} 없이 사용할 수 있습니다.

정리하면 클로저 인자에 @autoclosure를 선언하면 함수가 이미 클로저 인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있습니다.
단, 자동클로저를 너무 남용하면 코드를 이해하기 어려워 질 수 있습니다. 그래서 문맥과 함수 이름이 autoclosure를 사용하기에 분명해야 합니다.

자동클로저@autoclosure는 이스케이프@escaping와 같이 사용할 수 있습니다. 동작에 대한 설명은 코드에 직접 주석을 달았습니다.

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []        //  클로저를 저장하는 배열을 선언
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
} // 클로저를 인자로 받아 그 클로저를 customerProviders 배열에 추가하는 함수를 선언
collectCustomerProviders(customersInLine.remove(at: 0))    // 클로저를 customerProviders 배열에 추가
collectCustomerProviders(customersInLine.remove(at: 0))

print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."        // 2개의 클로저가 추가 됨
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")    // 클로저를 실행하면 배열의 0번째 원소를 제거하며 그 값을 출력
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

collectCustomerProviders함수의 인자 customerProvider는 @autoclosure이면서 @escaping로 선언되었습니다. @autoclosure로 선언됐기 때문에 함수의 인자로 리턴값 String만 만족하는 customersInLine.remove(at: 0)형태로 함수 인자에 넣을 수 있고, 이 클로저는 collectCustomerProviders함수가 종료된 후에 실행되는 클로저 이기 때문에 인자 앞에 @escaping 키워드를 붙여주었습니다.

복잡해 보이지만 결론적으로 escaping 함수 밖으로 꺼내기 위해서, autoclosure는 자동으로 클로져를 만들어 줌으로써 간결한 표현으로 만드는 것이라고 할 수 있겠습니다.


오늘은 스위프트 공식문서에서 Closures를 읽어보았습니다~
사용하는 것인데도, 이렇게보니 생각보다 어려워보이네요!
다음에는 Enumerations를 읽어보도록 하겠습니다!

감사합니다🙇🏻‍♀️

profile
한달 차 iOS 개발자입니다🐥

0개의 댓글