🔥 탈출 클로저

614자
8분

클로저가 함수의 인자로 전달되었지만, 함수가 반환된 후에 호출될 때 해당 클로저는 함수를 탈출(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를 캡처할 때는 강한 참조 사이클을 만들지 않도록 주의해야 한답니다.