🔥 탈출 클로저
클로저가 함수의 인자로 전달되었지만, 함수가 반환된 후에 호출될 때 해당 클로저는 함수를 탈출(escape)
한다고 말해요. 그래서 탈출 클로저(Escaping Closures)라고 합니다. 함수의 매개변수 중 하나로 클로저를 받는 함수를 선언할 때, 매개변수 타입 앞에 @escaping
을 작성하여 해당 클로저가 탈출할 수 있음을 나타낼 수 있답니다.
탈출하는 클로저
클로저가 탈출하는 한 가지 방법은 함수 외부에서 정의된 변수에 저장되는 것이에요. 예를 들어, 비동기 작업을 시작하는 많은 함수들은 완료 핸들러로 클로저 인자를 취합니다. 함수는 작업을 시작한 후 반환되지만, 클로저는 작업이 완료될 때까지 호출되지 않아요. 이런 경우 클로저는 나중에 호출되기 위해 탈출할 필요가 있죠. 아래 코드를 봐 주세요:
var completionHandlers: [() -> Void] = [] func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) { completionHandlers.append(completionHandler) }
swift
someFunctionWithEscapingClosure(_:)
함수는 클로저를 인자로 받아 함수 외부에서 선언된 배열에 추가하네요. 만약 이 함수의 매개변수에 @escaping
을 표시하지 않으면, 컴파일 타임 오류가 발생할 거예요.
탈출 클로저와 참조 사이클
self
를 참조하는 탈출 클로저는 self
가 클래스의 인스턴스를 참조할 때 특별한 고려가 필요해요. 탈출 클로저에서 self
를 캡처하면 실수로 강한 참조 사이클을 쉽게 만들 수 있기 때문이죠. 참조 사이클에 대한 정보는 Automatic Reference Counting을 참고하세요.
보통 클로저는 클로저 본문에서 변수를 사용하여 암시적으로 변수를 캡처하지만, 이 경우에는 명시적이어야 해요. self
를 캡처하려면 사용할 때 self
를 명시적으로 작성하거나 클로저의 캡처 목록에 self
를 포함시켜야 합니다. self
를 명시적으로 작성하면 의도를 표현할 수 있고, 참조 사이클이 없는지 확인하는 것을 상기시켜 준답니다.
아래 코드에서 someFunctionWithEscapingClosure(_:)
에 전달된 클로저는 self
를 명시적으로 참조해요. 반면에 someFunctionWithNonescapingClosure(_:)
에 전달된 클로저는 탈출하지 않는 클로저로, 암시적으로 self
를 참조할 수 있어요.
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) // "200" 출력 completionHandlers.first?() print(instance.x) // "100" 출력
swift
아래는 클로저의 캡처 목록에 self
를 포함하여 self
를 캡처하고, 암시적으로 self
를 참조하는 버전의 doSomething()
이에요:
class SomeOtherClass { var x = 10 func doSomething() { someFunctionWithEscapingClosure { [self] in x = 100 } someFunctionWithNonescapingClosure { x = 200 } } }
swift
self
가 구조체나 열거형의 인스턴스라면, 항상 self
를 암시적으로 참조할 수 있어요. 하지만 self
가 구조체나 열거형의 인스턴스일 때는 탈출 클로저에서 self
에 대한 변경 가능한 참조를 캡처할 수 없답니다. Structures and Enumerations Are Value Types에서 설명한 것처럼, 구조체와 열거형은 공유 변경을 허용하지 않아요.
struct SomeStruct { var x = 10 mutating func doSomething() { someFunctionWithNonescapingClosure { x = 200 } // 가능 someFunctionWithEscapingClosure { x = 100 } // 오류 } }
swift
위 예제에서 someFunctionWithEscapingClosure
함수를 호출하는 것은 오류인데, 이는 변경 메서드 내부에 있어서 self
가 변경 가능하기 때문이에요. 이는 구조체의 경우 탈출 클로저가 self
에 대한 변경 가능한 참조를 캡처할 수 없다는 규칙을 위반하는 거죠.
또 다른 예제를 같이 볼게요. 음식 배달 앱을 개발 중이라고 가정해 보겠습니다.
// 주문 완료 핸들러 타입 정의 typealias OrderCompletionHandler = (Result<String, Error>) -> Void // 음식 주문 함수 - 탈출 클로저를 매개변수로 받음 func placeOrder(menuItems: [String], completionHandler: @escaping OrderCompletionHandler) { // 주문 처리 비동기 작업 시작 DispatchQueue.global().async { // 주문 처리하는데 2초 걸린다고 가정 Thread.sleep(forTimeInterval: 2.0) // 주문 완료 시 완료 핸들러 호출 completionHandler(.success("주문이 완료되었습니다!")) } } // 음식 주문 함수 호출 placeOrder(menuItems: ["햄버거", "감자튀김"]) { result in switch result { case .success(let message): print(message) case .failure(let error): print(error.localizedDescription) } } print("주문 처리 중...") /* 출력: 주문 처리 중... 주문이 완료되었습니다! */
swift
위 코드에서 placeOrder(menuItems:completionHandler:)
함수는 @escaping
클로저를 매개변수로 받아요. 이 함수는 주문을 처리하는 비동기 작업을 시작하고 바로 반환되는데, 완료 핸들러는 나중에 비동기 작업이 완료되면 호출되죠.
완료 핸들러가 탈출 클로저이기 때문에, 함수가 반환된 후에도 클로저가 유지되고 비동기 작업이 완료될 때 호출될 수 있습니다. 만약 @escaping
을 표시하지 않으면, 완료 핸들러가 함수 밖으로 탈출할 수 없다는 컴파일 오류가 발생할 거예요.
정리하면, 탈출 클로저는 함수에서 반환된 후에도 호출될 수 있기 때문에 비동기 작업에서 유용하게 사용될 수 있어요. 그렇지만 탈출 클로저에서 self
를 캡처할 때는 강한 참조 사이클을 만들지 않도록 주의해야 한답니다.