iOS/Swift

Swift) 클로저가 멀까??

Brad_Heo 2022. 7. 13. 01:05

클로저

클로저란??

  • 사용자의 코드안에서 전달되어 사용할 수 있는 로직을 가진 중괄호 "{}"로 구분된 코드의 블록, 일급 객체의 역활을 수행
  • 참조 타입이다
  • 함수는 클로저의 한 형태로, 이름이 있는 클로저!!
일급객체??
일급 객체는 전달인자를 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며, 함수의 반환값으로도 될 수 있다.

클로저 표현 방식

{ (인자들) -> 반환타입 in 
    로직구현
}

그럼 클로저를 쓰는 이유가 멀까??

코드를 효율적으로 작성하는데 도움을 주기 떄문에
대표적으로 두가지가 있을 수도 있겠다.

  1. 고참함수
    인풋으로 줄 함수를 따로 만들어 사용할 순 있지만 고차함수를 사용하는 그 자리에서 바로 클로저를 만들어 인풋을 줄 수도 있다. 이때 클로저를 사용한다.

고차함수??
인풋으로 함수를 받을 수 있는 함수
대표적은 map, filter

  1. completion block
    어떠한 테스트가 완료되었을 때 수행하는 블록
    클로저는 비동기 작업에 많이 사용되는데
    비동기적인 어떠한 테스트가 끝났을때, 그 후 수행해야하는 블록을 클로저로 작성하면 편리하다.

completion block에 대해 조금 더 알아보자

클로저를 통해 비동기 콜백을 작성하는 경우, 현재 상태를 미리 획득(저장)하지 않으면 실제로 클로저의 기능을 실행하려는 순간, 상수나 변수가 이미 메모리에 존재하지 않을 수도 있다.
따라서 이 떄 필요한 것이 바로 클로저의 값 갭쳐 기능이다.

값 갭쳐??

말 그대로 값을 캡쳐해서 사용하는 것을 말한다.
값을 캡쳐해서 가지고 있다가, 필요할 때마다 사용하는 것

클로저의 종류

Escaping Closure

클로저가 함수의 인자로 전달되지만 함수 밖에서 실행되는 것(함수가 반환된 후 실행 되는 것)

  • 비동기로 실행되는 경우
  • completionHandler로 사용되는 클로저의 경우
  • 함수 사이에 실행 순서를 정할 수 있다.

Non-Escaping Closure

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

클로저가 실행되는 순서

  1. 클로저가 runClosure() 함수의 closure 인자로 전달됨
  2. 함수 안에서 closure()가 실행됨
  3. runClosure() 함수가 값을 반환하고 종료됨

이렇게 클로저가 함수가 종료되기 전 실행되기 때문에 closure는 Non-Escaping 클로저!!

Escaping Closure

class ViewModel {
    var completionhandler: (() -> Void)? = nil

    func fetchData(completion: @escaping () -> Void) {
        completionhandler = completion
    }
}

이스케이핑 클로저가 실행되는 순서

  1. 클로저가 fetchData() 함수의 completion 인자로 전달
  2. 클로저 completioncompletionhandler 변수에 저장됨
  3. fetchData() 함수가 값을 반환하고 종료됨
  4. 클로저 completion은 아직 실행되지 않음

completion은 함수의 실행이 종료되기 전에 실행되지 않기 때문에, escaping 클로저, 다시 말해 함수 밖(escaping)에서 실행되는 클로저

사용 예
비동기로 실행되는 HTTP Request CompletionHandler

func makeRequest(_ completion: @escaping (Result<(Data, URLResponse), Error>) -> Void) {
  URLSession.shared.dataTask(with: URL(string: "http://jusung.github.io/")!) { data, response, error in
    if let error = error {
      completion(.failure(error))
    } else if let data = data, let response = response {
      completion(.success((data, response)))
    }
  }
}

makeRequest() 함수에서 사용되는 completion 클로저는 함수 실행 중 즉시 실행되지 않고, URL 요청이 끝난 후 비동기로 실행 됨.
이 경우에도 competion의 타입에 @escaping을 붙여서 escaping 클로저라는 것을 명시 해줘야 함

보통 클로저가 다른 변수에 저장되어 나중에 실행되거나 비동기로 실행될 때 escaping 클로저가 사용됨

공식문서의 예제를 보자

var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}
  • 함수에서 인자로 전달된 completionHandler는 someFuctionWithEscapingClosure함수가 끝나고 나중에 처리.
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 } // 명시적으로 self를 적어줘야 한다.
    someFunctionWithNonescapingClosure { x = 200 }
  }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x) // 200
completionHandlers.first?()
print(instance.x) // 100

AutoClosure

자동 클로저는 인자 값이 없으며 특정 표현을 감싸 다른 함수에 전달 인자로 사용할 수 있는 클로저를 말함. 자동 클로저는 클로저를 실행하기 전 실제 실행되지않는다

즉 계산이 복잡한 연산을 하는데 유용..

var customersInLine = ["Chris", "Alex", "Ewa", "Barry", "Daniella"]
print(customersInLine.count) // 5
let customerProvider = { customersInLine.remove(at: 0) } // 해당 코드가 지나도 count가 줄지 않는다.
print(customersInLine.count) // 5
// customerProvider가 실행되었을때만 동작
print("Now serving \(customerProvider())!") // "Now serving Chris!"
print(customersInLine.count) // 4

자동클로저를 함수의 인자 값으로 넣는 예제는 아래와 같음.

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

Servo 함수는 인자로 () -> String(인자가 없고, String을 반환하는 클로저)를 가짐.

그리고 이 함수를 실행할 때 serve(customer:{ customersInLine.remove(at:0) } )와 같이 클로저를 명시적으로 직접 넣을 수 있음

@autoclosure 키워드를 이용해서 보다 간결하게 사용할 수 있다.

@autoclosure는 @escaping과 같이 사용할 수 있다.

// customersInLine is ["Ewa", "Barry", "Daniella"]
func serve(customer customerProvider: @autoclosure () -> String) {
  print("Now serving \(customerProvider())!")
}
serve(customer: customersInLine.remove(at: 0)) // "Now serving Ewa!"
var customersInLine = ["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.") // 2개의 클로저가 추가 됨
// Prints "Collected 2 closures."
for customerProvider in customerProviders {
  print("Now serving \(customerProvider())!") // 클로저를 실행하면 배열의 0번째 원소를 제거하며 그 값을 출력
}
// Prints "Now serving Barry!"
// Prints "Now serving Daniella!"

정리
클로저는 고차함수와 비동기 콜백에 많이 이용하는데, 그 중에서도 비동기 콜백에 이용되는 이유는 클로저의 값 캡쳐 기능 때문..
값 갭쳐는 값을 미리 획득해서 선언할 때마다 획득한 값의 참조에 연산을 누적하는 것!

참조 사이트
https://seolhee2750.tistory.com/117

https://medium.com/@jgj455/%EC%98%A4%EB%8A%98%EC%9D%98-swift-%EC%83%81%EC%8B%9D-closure-aa401f76b7ce