본문 바로가기
Swift

알쓸스잡 - 8

by songmoro 2025. 2. 2.
728x90

Closure

클로저는 코드에서 주변에 전달 및 사용할 수 있는 함수 블록

  • Swift의 클로저는 다른 프로그래밍 언어에서 클로저, 익명 함수, 람다, 블록과 유사
  • 정의된 컨텍스트 내에서 모든 상수와 변수에 대한 참조를 캡처하고 저장
    • 이러한 상수와 변수를 폐쇄(closing over)라고 한다.
  • Swift는 캡처의 모든 메모리 관리를 처리

 

함수 페이지에서 소개한 전역, 중첩 함수는 클로저의 특별한 케이스로 클로저는 3가지 형태 중 하나를 취한다.

  • 전역 함수는 이름을 가지고, 어떠한 값도 캡처하지 않는 클로저
  • 중첩 함수는 이름을 가지고, 둘러싼 함수로부터 값을 캡처할 수 있는 클로저
  • 클로저 표현식은 이름을 가지지 않고, 주변 컨텍스트에서 값을 캡처할 수 있는 클로저
    • 클로저 표현식은 간단하고 깔끔한 구문을 장려하는 최적화로 깔끔하고 명확한 스타일을 지닌다.
      • 최적화에는 컨텍스트에서 파라미터와 반환 값 타입 유추, 단일 표현식 클로저의 암시적 반환, 약식 인수 이름, 후행 클로저 구문을 포함

 

Closure Expression

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

 

예:

func backward(_ s1: String, _ s2: String) -> Bool {
    return s1 > s2
}

let names = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
var reversedNames = names.sorted(by: backward)
// reversedNames is equal to ["Ewa", "Daniella", "Chris", "Barry", "Alex"]
  • sorted(by: ) 함수에 (String, String) -> Bool 타입의 함수를 사용하는 클로저를 사용한 모습

 

Closure Expression Syntax

클로저 표현구는 아래와 같은 일반적인 형태를 띤다.

{ (<#parameters#>) -> <#return type#> in
   <#statements#>
}

 

클로저 표현구의 파라미터는 in-out 파라미터일 수 있지만, 기본 값을 가질 수 없다.

  • 가변 파라미터의 이름을 선언하면, 가변 파라미터를 사용할 수 있다.

 

아래 예는 backward(::) 함수의 클로저 표현 버전(인라인 클로저)

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

 

Inferring Type From Context

정렬 클로저는 메서드에 인수로 전달되기 때문에 Swift는 파라미터 타입과 반환되는 값의 타입을 유추할 수 있다.

  • sorted(by:) 함수는 문자열 배열에서 호출되므로, 인수는 (String, String) -> Bool 타입의 함수
    • 모든 타입을 유추할 수 있기 때문에, 클로저 표현식 정의에 일부러 작성할 필요가 없음
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )

 

Implicit Returns from Single-Expression Closures

단일 표현 클로저는 return 키워드를 생략하여 단일 표현식으로 암시적으로 값을 반환할 수 있다.

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

 

Shorthand Argument Names

Swift는 인라인 클로저에 $0, $1, $2 등 클로저의 인수 값으로 참조하는 데 사용할 수 있는 짧은 인수 이름을 제공한다.

  • 클로저 표현식에 짧은 인수 이름을 사용한다면 선언에 클로저의 인수 리스트를 생략할 수 있고, 짧은 인수 이름의 수와 타입은 함수 타입에서 유추된다.
  • 클로저 표현식이 본문으로 전체가 구성되기 때문에 in 키워드 또한 생략할 수 있다.
reversedNames = names.sorted(by: { $0 > $1 } )

 

Operator Methods

Swift의 String 타입은 보다 큰 연산자(>)의 문자열 별 구현을 String 타입의 파라미터 2개가 있는 메서드로 정의하고, Bool 타입의 값을 반환한다.

  • (String, String) -> Bool

 

이는 sorted(by:) 함수에 필요한 함수 타입과 일치하며, 따라서 간단하게 보다 큰 연산자를 전달할 수 있다.

  • Swift는 문자열 특정 구현을 사용하기 원한다고 유추한다.
reversedNames = names.sorted(by: >)

 

Trailing Closure

함수가 여러 개의 클로저를 가지고 있다면, 첫 번째 후행 클로저의 인수 라벨을 생략하고, 남은 후행 클로저의 라벨은 표기한다.

  • 이러한 방식으로 함수를 작성하면, 두 상황을 모두 처리하는 하나의 클로저를 사용하는 대신, 성공과 오류를 처리하는 코드를 명확하게 분리할 수 있다.
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.")
}

 

Capturing Values

클로저는 둘러싸인 컨텍스트에서 상수와 변수를 캡처할 수 있다.

  • 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도, 본문 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있다.

 

값을 캡처할 수 있는 가장 간단한 클로저 형태는 다른 함수의 컨텍스트 내에 작성하는 중첩 함수

  • 중첩 함수는 바깥 함수의 어떠한 인수라도 캡처할 수 있고, 바깥 함수 내에 정의된 상수와 변수를 캡처할 수도 있다.

 

아래 예는 incremeter라는 중첩 함수가 포함된 makeIncrementer라는 함수의 예

  • 중첩된 incrementer() 함수는 컨텍스트의 runningTotal, amount 2개의 값을 캡처
  • 캡처한 후 incrementer는 호출될 때마다, amount로 runningTotal을 증가시키는 클로저로 makeIncrementer에 의해 반환됨
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var runningTotal = 0
    func incrementer() -> Int {
        runningTotal += amount
        return runningTotal
    }
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)
let incrementBySeven = makeIncrementer(forIncrement: 7)

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

incrementBySeven()
// returns a value of 7
incrementByTen()
// returns a value of 40

 

Closures Are Reference Types

위 예제에서 incrementBySeven, incrementByTen은 상수지만, 참조하는 클로저는 캡처한 runningTotal 변수를 계속 증가시킨다.

  • 이는 함수와 클로저가 참조 타입이기 때문

 

이 말의 의미는 서로 다른 2개의 상수 혹은 변수에 클로저를 할당한다면, 2개의 상수 혹은 변수는 모두 같은 클로저를 참조한다는 의미

let alsoIncrementByTen = incrementByTen

alsoIncrementByTen()
// returns a value of 50
incrementByTen()
// returns a value of 60

 

Escaping Closures

함수에 인수로 클로저를 전달하지만, 함수가 반환된 후 호출되는 클로저를 "함수를 탈출시킨다"라고 한다.
클로저를 파라미터로 갖는 함수를 선언할 때, 이 클로저는 탈출을 허락한다는 의미로 파라미터의 타입 전에 @escaping을 작성할 수 있다.

 

예를 들어, 비동기 작업을 시작하는 함수는 컴플리션 핸들러로 클로저를 사용한다.
이 함수는 작업을 시작한 후 반환되지만, 작업이 완료될 때까지 클로저가 호출되지 않는다.

  • 클로저가 나중에 호출되려면 escaping 되어야 한다.

 

탈출 클로저를 사용하기 위해선 강한 참조 사이클을 고려하기 위해 self를 사용해야 한다.

var completionHandlers: [() -> Void] = []

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

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}


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


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

 

수행 순서

  • instance에 SomeClass 인스턴스 할당
  • instance의 doSomething 함수 실행
    • someFunctionWithEscapingClosure가 completionHandlers에 추가됨
    • someFunctionWithNonescapingClosure가 실행됨
      • instance.x = 10 -> 200
  • print(instance.x) // 200
  • completionHandlers의 첫 번째 함수를 실행
    • instance.x = 200 -> 100
  • print(instance.x) // 100

 

Autoclosures

자동 클로저는 자동으로 생성되는 클로저

  • 함수에 인수로 전달되는 표현식을 래핑 하기 위해 사용
  • 인수를 가지지 않음
  • 호출될 때 내부에 래핑 된 표현식의 값을 반환

 

클로저가 호출될 때까지 코드 내부 실행이 되지 않기 때문에, 자동 클로저는 판단을 지연시킬 수 있다.

  • 판단 지연은 코드 판단 시기를 제어할 수 있기 때문에, 사이드 이펙트가 존재하거나 계산이 오래 걸리는 코드에 유용
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"

클로저 내부 코드(customerProvider)에 의해 customersInLine 배열의 첫 번째 요소가 삭제되지만, 클로저가 실제로 호출되기 전까진 삭제되지 않는다.

  • customerProvider: () -> String

 

// 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(customer:) 함수는 소비자의 이름을 반환하는 명시적 클로저를 가짐

  • 대신 파라미터 타입에 @autoclosure 속성을 표기하면 자동 클로저를 가지며, 클로저 대신 String 인수를 받는 것처럼 함수를 호출할 수 있다.

 

// 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!"

 

또한, 탈출 클로저와 자동 클로저를 동시에 사용할 수 있다.

// customersInLine is ["Barry", "Daniella"]
var customerProviders: [() -> String] = []
func collectCustomerProviders(_ customerProvider: @autoclosure @escaping () -> String) {
    customerProviders.append(customerProvider)
}
collectCustomerProviders(customersInLine.remove(at: 0))
collectCustomerProviders(customersInLine.remove(at: 0))


print("Collected \(customerProviders.count) closures.")
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
    print("Now serving \(customerProvider())!")
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

 

참고

https://bbiguduk.github.io/swift-book-korean/documentation/the-swift-programming-language-korean/closures/
https://docs.swift.org/swift-book/documentation/the-swift-programming-language/closures/

728x90

'Swift' 카테고리의 다른 글

Swift: async let 수행 순서  (0) 2025.03.30
Swift Concurrency  (0) 2025.02.04
알쓸스잡 - 7  (0) 2025.02.01
알쓸스잡 - 6  (0) 2025.01.21
알쓸스잡 - 5  (0) 2025.01.17