티스토리 뷰

프로그래밍언어/Swift

Swift 클로저 (Closure)

데니 Denny 2020. 2. 23. 21:04
반응형
SMALL

이번 포스트에서는 Swift에서의 Closure에 대해 살펴봅도록 하겠습니다.

 

클로저(Closure)는 코드에 전달되어 사용할 수 있는 독립적인 기능 블록입니다. Swift의 클로저는 C 및 Objective-C의 블록 및 다른 프로그래밍 언어의 람다(Lambda)와 유사합니다.

 

클로저(Closure)는 정의된 컨텍스트에서 상수(Constant) 및 변수 (Variable) 에 대한 참조를 캡처하고 저장할 수 있습니다. 이것을 상수(Constant) 와 변수(Variable) 를 닫는 것으로 알려져 있습니다. Swift는 이 캡쳐와 관련한 모든 메모리를 자동으로 처리합니다.

 

캡쳐의 개념에 대해서는 후반부에서 살펴보도록 하겠습니다.

 

함수에 도입된 전역 함수와 중첩 함수는 실제로 특수한 클로저입니다. 클로저는 세 가지 형식 중 하나를 사용합니다.

1. 전역 함수는 이름이 있고 값을 캡처하지 않는 클로저입니다.
2. 중첩 함수는 이름이 있으며 폐쇄 함수에서 값을 캡처 할 수있는 클로저입니다.
3. 클로저 표현식은 주변 컨텍스트에서 값을 캡처 할 수있는 간단한 구문으로 작성된 명명되지 않은 클로저입니다.

Swift의 클로저 표현은 깔끔하고 명확한 스타일을 가지고 있으며 일반적인 시나리오에서 짧고 깔끔한 구문을 권장하는 최적화 기능이 있습니다. 이러한 최적화에는 다음이 포함됩니다.

- 컨텍스트에서 매개 변수 및 리턴 값 유형 유추
- 단일 표현식 클로저에서 암시적인 반환
- 속기 인수 이름
- 후행 폐쇄 구문

클로저 표현 (Closure Expressions)

클로저 표현식은 간단하고 집중적 인 구문으로 인라인 클로저를 작성하는 방법입니다. 클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형식으로 클로저를 작성하기 위한 몇 가지 구문 최적화를 제공합니다.

정렬 메소드 (The Sorted Method)

Swift의 표준 라이브러리는 sorted (by :)라는 메소드를 제공합니다.이 메소드는 사용자가 제공 한 정렬 클로저의 출력에 따라 알려진 유형의 값 배열을 정렬합니다. 정렬 프로세스가 완료되면 sorted (by :) 메소드는 요소가 올바른 정렬 순서로 이전 유형과 크기 및 크기가 같은 새 배열을 리턴합니다. 원래 배열은 sorted (by :) 메소드로 수정되지 않습니다.

아래의 클로저 표현식 예제는 sorted (by :) 메소드를 사용하여 문자열 값의 배열을 알파벳 역순으로 정렬합니다. 정렬 할 초기 배열은 다음과 같습니다.

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

sorted(by:) 메소드는 배열의 콘텐츠와 같은 타입을 갖고 두개의 인자를 갖는 클로저를 인자로 사용합니다. name의 콘텐츠는 String 타입이므로 (String, String) -> Bool 의 타입의 클로저를 사용해야 합니다.

 

클로저를 제공하는 일반적인 방법은 함수를 하나 만드는 것입니다. 위 타입을 만족 시키는 함수를 하나 만들면 정렬에 인자로 넣을 수 있는 클로저를 만들 수 있습니다.

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)에 넣으면 원본 배열의 순서가 바뀐 배열을 정렬 결과로 얻을 수 있습니다. 비교하는 클로저를 사용할 때 긴 코드를 사용했는데, 앞으로 클로저의 다양한 문법 및 사용에 대해 알아 보겠습니다.

클로저 표현 문법 (Closure Expression Syntax)

Swift에서 일반적으로 클로저의 표현은 다음과 같이 표현합니다.

인자로 넣을 parameters, 인자 값으로 처리할 내용을 기술하는 statements 그리고 return type입니다. 앞의 backward클로저를 이용해 배열을 정렬하는 코드는 클로저 표현을 이용해 다음과 같이 바꿀 수 있습니다. 클로저 표현식 구문의 매개 변수는 입력 매개 변수 일 수 있지만 기본값을 가질 수는 없습니다. 가변 매개 변수의 이름을 지정하면 가변 매개 변수를 사용할 수 있습니다. 튜플은 매개 변수 타입 및 리턴 타입으로도 사용할 수 있습니다.

아래 예제는 위의 reverse (_ : _ :) 함수의 클로저 표현식 버전을 보여줍니다.

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

이 인라인 클로저에 대한 매개 변수 및 리턴 타입의 선언은 reverse (_ : _ :) 함수의 선언과 동일합니다. 두 경우 모두 (s1 : String, s2 : String)-> Bool로 작성됩니다.

그러나 인라인 클로저 표현식의 경우 매개 변수 및 리턴 타입은 중괄호 안에 작성됩니다.

클로저 본문의 시작은 in 키워드에 의해 소개됩니다. 이 키워드는 클로저 매개 변수 및 리턴 타입의 정의가 완료되었으며 클로저 본문이 곧 시작될 것임을 나타냅니다. 클로저 본문이 너무 짧기 때문에 한 줄로 작성할 수도 있습니다.

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

문맥에서 타입 추론 (Inferring Type From Context)

정렬 클로저는 메소드에 인수로 전달되므로 Swift는 매개 변수의 유형과 반환하는 값의 유형을 유추 할 수 있습니다.

sorted (by :) 메소드는 문자열 배열에서 호출되므로 인수는 (String, String)-> Bool 타입의 함수이어야 합니다.

즉, (String, String)Bool 유형을 클로저 표현식 정의의 일부로 작성할 필요가 없습니다. 모든 유형이 유추 될 수 있으므로 매개 변수 이름 주위의 리턴 화살표 (->) 및 괄호도 생략 할 수 있습니다.

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

타입의 모호함을 피하기 위해 타입을 명시할 수도 있습니다.

단일 표현 클로저에서의 암시적 반환 (Implicit Returns from Single-Express Closures)

단일 표현 클로저에서는 return 키워드를 생략할 수 있습니다.

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

인자 이름 축약 (Shorthand Arguments Names)

Swift는 인라인 클로저에 축약 형 인수 이름을 자동으로 제공하여 클로저의 인수 값을 $ 0, $ 1, $ 2 등으로 참조 할 수 있습니다.

클로저 표현식 내에서 이러한 속기 인수 이름을 사용하는 경우 클로저의 인수 목록을 정의에서 생략 할 수 있으며 속기 인수 이름의 수와 타입은 예상 함수 타입에서 유추됩니다. in 키워드는 클로저 표현식이 본문으로 만 구성되므로 생략 할 수도 있습니다.

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

여기서 $ 0과 $ 1은 클로저의 첫 번째 및 두 번째 문자열 인수를 나타냅니다.

첫 번째 값과 두 번째 값을 비교해서 첫 번째 값이 더 큰 지 체크해서 그 결과를 Bool형으로 반환한다고 보시면 됩니다.

연산자 메소드 (Operator Methods)

실제로 클로저 표현을 작성하는 더 짧은 방법이 있습니다. Swift의 String 타입은 보다 큰 연산자 (>)의 문자열 별 구현을 String 타입의 두 매개 변수가 있는 메소드로 정의하고 Bool 타입의 값을 리턴합니다. 이것은 sorted (by :) 메소드에 필요한 메소드 타입과 정확히 일치합니다. 따라서 단순히 보다 큰 연산자를 전달하면 Swift에서 문자열 별 구현을 사용하려는 것으로 추론합니다.

reversedNames = names.sorted(by: >)

후위 클로저 (Trailing Closures)

만약 함수의 마지막 인자로 클로저를 넣고 그 클로저가 길다면 후위 클로저를 사용할 수 있습니다. 이런 형태의 함수와 클로저가 있다면 함수의 마지막 인수로 클로저 표현식을 함수에 전달해야하고 클로저 표현식이 긴 경우 대신 후위 클로저로 작성하는 것이 유용 할 수 있습니다. 후위 클로저는 여전히 함수에 대한 인수 인 경우에도 함수 호출의 괄호 뒤에 표시됩니다. 후위 클로저 구문을 사용하는 경우 함수 호출의 일부로 폐쇄에 대한 인수 레이블(Arguments Label)을 작성하지 않습니다.

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(by: { $0 > $1 } )

// 후위 클로저로 변환
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]

이 값을 배열의 map(_:)메소드를 이용해 특정 값을 다른 특정 값으로 매핑하는 할 수 있는 클로저를 구현합니다.

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값의 변환이 가능합니다. 기본적으로 함수와 클로저에 넘겨지는 인자 값은 상수입니다.

NOTE
digitNames 사전의 첨자 호출 뒤에는 느낌표 (!)가옵니다. 사전 첨자는 키가 없으면 사전 조회가 실패 할 수 있음을 나타내는 선택적 값을 반환하기 때문입니다. 위의 예에서 number % 10은 항상 digitNames 사전에 유효한 첨자 키가 될 것이므로 느낌표를 사용하여 첨자의 선택적인 반환 값에 저장된 문자열 값을 강제로 해제합니다.

값 캡쳐 (Capturing Values)

클로저는 정의된 주변 컨텍스트에서 상수와 변수를 캡처 할 수 있습니다. 그런 다음 상수 및 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도 클로저는 본문 내에서 해당 상수 및 변수의 값을 참조하고 수정할 수 있습니다.

Swift에서 값을 캡처 할 수있는 가장 간단한 클로저 형식은 다른 함수의 body 안에 작성된 중첩 함수입니다. 중첩 함수는 외부 함수의 인수를 캡처 할 수 있으며 외부 함수 내에 정의된 상수 및 변수도 캡처 할 수 있습니다.

다음은 incrementer라는 중첩 함수가 포함 된 makeIncrementer라는 함수의 예입니다. incrementer 함수는 주변 컨텍스트에서 runningTotal과 amount의 두 값을 캡처합니다. 이 값을 캡처 한 후, makeIncrementer는 이를 호출 할 때마다 runningTotalamount만큼 증가시키는 클로저로 인크 리 멘터를 리턴합니다.

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

이 함수는 makeIncrementer 함수 안에서 incrementer함수를 호출하는 형태로 중첩 함수입니다. 클로저의 인자와 반환 값이 보통의 경우와 달라 어렵게 보일 수 있는데, 이것도 역시 쪼개 보면 어렵지 않습니다.

인자와 반환 값 (forIncrement amount: Int) -> () -> Int 중에 처음 -> 를 기준으로 앞의 (forIncrement amount: Int) 부분이 인자 값이고 뒤 () -> Int는 반환 값입니다. 이것은 반환 값이 클로저인 형태입니다. 반환 값을 인자가 없고 Int형의 클로저를 반환한다는 의미입니다.

함수 안의 incrementer함수만 따로 살펴 봅도록 하겠습니다.

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

runningTotalamount도 없습니다. 하지만 이 함수는 돌아갑니다. 그것은 runningTotalamount가 캡쳐링 되서 그런 것 입니다.

최적화 이유로 Swift는 만약 더 이상 클로저에 의해 값이 사용되지 않으면 그 값을 복사해 저장하거나 캡쳐링 하지 않습니다. Swift는 또 특정 변수가 더 이상 필요하지 않을 때 제거하는 것과 관련한 모든 메모리 관리를 알아서 처리합니다.
let incrementByTen = makeIncrementer(forIncrement: 10)

makeIncrementer함수는 클로저를 반환합니다.

여기서는 makeIncrementer 내부의 incrementer 함수를 실행하는 메소드를 반환합니다.

incrementByTen()
// 값으로 10을 반환합니다.
incrementByTen()
// 값으로 20을 반환합니다.
incrementByTen()
// 값으로 30을 반환합니다.

함수가 각기 실행 되지만 실제로는 변수 runningTotalamount가 캡쳐링 되서 그 변수를 공유하기 때문에 계산이 누적된 결과를 갖습니다. 만약 아래와 같이 새로운 클로저를 생성하면 어떻까요?

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

다른 클로저이기 때문에 고유의 저장소에 runningTotalamount를 캡쳐링 해서 사용합니다. 그래서 다른 값이 나옵니다. 그렇다면 여기서 이전의 클로저를 실행하면 어떻게 될까요?

incrementByTen()
// 값으로 40을 반환합니다.

다른 클로저이기 때문에 연산에 전혀 영향이 없습니다. 그냥 다른 저장소의 변수를 사용해 계산합니다.

클로저는 참조 타입 (Closures Are Reference Types)

위의 예에서 incrementalBySeven 및 incrementByTen은 상수이지만 이러한 상수가 참조하는 클로저는 여전히 캡처 한 runningTotal 변수를 증가시킬 수 있습니다. 함수와 클로저가 참조 유형이기 때문입니다.

함수 나 클로저를 상수 나 변수에 할당 할 때마다 실제로는 상수 나 변수를 함수 나 클로저에 대한 참조로 설정합니다. 위의 예에서 incrementalByTen이 클로저 자체의 내용이 아닌 상수를 나타내는 것이 클로저 선택입니다.

이는 또한 두 개의 다른 상수 또는 변수에 클로저를 할당하면 해당 상수 또는 변수가 모두 동일한 클로저를 참조함을 의미합니다.

let alsoIncrementByTen = incrementByTen
alsoIncrementByTen()
// 50을 반환합니다.

이스케이핑 클로저 (Escaping Closures)

클로저는 함수에 대한 인수로 클로저가 전달 될 때 함수를 이스케이프한다고하지만 함수가 반환 된 후에 호출됩니다.

클로저를 매개 변수 중 하나로 사용하는 함수를 선언하면 매개 변수 유형 앞에 @escaping을 작성하여 클로저가 이스케이프 될 수 있음을 나타낼 수 있습니다.

클로저가 벗어날 수 있는 한 가지 방법은 함수 외부에 정의된 변수에 저장하는 것입니다. 예를 들어, 비동기 작업을 시작하는 많은 함수는 클로저 인수를 completionHandler로 사용합니다. 함수는 작업을 시작한 후에 Return 되지만 작업이 완료 될 때까지 클로저가 호출되지 않습니다. 클로저를 escape 한 후에야 나중에 호출 할 수 있습니다. 예를 들면 다음과 같습니다.

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

위 함수에서 인자로 전달된 completionHandlersomeFunctionWithEscapingClosure 함수가 끝나고 나중에 처리 됩니다.

만약 함수가 끝나고 실행되는 클로저에 @escaping 키워드를 붙이지 않으면 컴파일시 오류가 발생합니다.

@escaping 를 사용하는 클로저에서는 self를 명시적으로 언급해야 합니다.

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

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"

자동클로저 (Autoclosures)

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

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를 선언하면 함수가 이미 클로저 인것을 알기 때문에 리턴값 타입과 같은 값을 넣어줄 수 있습니다.

NOTE
자동클로저를 너무 남용하면 코드를 이해하기 어려워 질 수 있습니다. 그래서 문맥과 함수 이름이 
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 키워드를 붙여주었습니다.

 

이번 포스트에서는 Swift의 클로저에 대해 살펴보았습니다.

반응형
LIST
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/04   »
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
글 보관함