서론
@escaping 키워드가 붙은 클로저를 본 적이 있으실 겁니다.
func withEscaping(completion: @escaping () -> Void) {
completion()
}
가장 대표적인 예는 completionHandler가 있습니다.
escaping 클로저는 non-escaping 클로저와 달리 아래 동작이 가능합니다.
- 파라미터로 전달된 클로저를 외부 변수/상수에 저장 가능
- 함수가 종료된 뒤 실행 가능
오늘 포스팅에서는 escaping에 대해 자세히 알아봅시다.
non-escaping 클로저
escaping 클로저에 대해 자세히 알아보기 전에 non-escaping 클로저에 대해 알아봅시다.
@escaping 이 붙지 않은 클로저는 모두 non-escaping 클로저인데요.
무슨 특징이 있을까요?
non-escaping는 파라미터로 전달된 클로저를 외부 변수/상수에 저장할 수 없습니다.
non-escaping은 탈출 불가능이라는 의미로, 여기서 탈출은 클로저 외부에서 사용되는 것을 의미합니다.
Swift에서 함수의 파라미터로 전달된 클로저는 함수 내부 지역 안에서만 사용이 가능합니다.
탈출 불가능이라는 의미가 바로 함수 내부를 탈출할 수 없다는 의미인 것이죠.
그래서 위 사진처럼 함수 외부의 변수에 넣게 되면 에러가 발생하게 되는 것입니다.
탈출 불가능하기 때문에 함수가 종료되기 전 반드시 전달 받은 클로저가 실행됩니다.
함수가 종료되기 전에 실행이 되므로 이 클로저를 외부에서는 사용할 수 없겠죠?
이제 non-escaping(탈출 불가능)의 의미에 대해 이해가 가셨나요?
escaping 클로저
escaping 클로저는 탈출 가능한 클로저입니다.
함수로부터 Escape 한다는 것은 해당 함수의 인자로 클로저가 전달되고 함수가 반한된 후에 실행되는 것을 의미합니다.
함수의 인자를 함수 밖에서 사용하는 것은 scope 개념을 무시합니다.
이래서 탈출이라고 말하는거죠.
함수의 실행을 끊고 탈출한다는 의미가 아니라 클로저를 외부로 보낼 수 있다는 의미입니다.
헷갈리면 안 돼요!
파라미터로 전달 받은 클로저 왼쪽에 @escaping을 붙여주면 escaping 클로저가 됩니다.
@escaping을 작성하는 위치가 헷갈리시면 "closure는 탈출 가능하고 () -> Void형이다." 라고 생각하면 헷갈리지 않을 거에요.
@escaping을 붙여줬더니 에러도 사라진 것을 볼 수 있습니다.
@escaping이 붙음으로서 탈출이 가능해지고 외부에서도 사용이 가능해진 상태가 된 것입니다.
escaping 클로저는 왜 쓸까?
비동기 처리
escaping 클로저는 비동기 처리를 위해 사용합니다.
함수의 실행 순서를 정할 수 있기 때문에 함수의 실행 순서를 보장 받을 수 있습니다.
A -> B -> C 순서로 실행된다는 것을 보장할 수 있다는 의미입니다.
이러한 장점 덕에 서버에 요청을 보내고 응답이 왔을 때 결과를 반환하기 위해서 completionHandler로서 이용합니다.
func getVersion(completionHandler: @escaping (Bool, Any) -> Void) {
requestGet(url: serverIP + "/version", completionHandler: { (result, response) in
completionHandler(result, response)
})
}
서버에서 최신 버전을 가져오는 메서드입니다.
네트워크 상황에 따라서 함수가 끝나기 전에 서버에서 response가 오지 않을 수 있겠죠?
근데 non-escaping은 함수가 끝나기 전에 전달 받은 클로저가 반드시 실행이 되야 합니다.
그러면 response가 오기도 전에 클로저가 실행이 되니 원하는 동작이 되지 않을 거에요.
request 요청 -> 함수 종료 -> response 받음 -> 클로저 실행 순서를 보장 받아야 하고
함수가 끝나도 실행이 되야 하기 때문에 escaping 클로저로 비동기 처리를 하는 것입니다.
이해가 가시나요??
static 메서드
HTTP 통신에서 completionHandler로 사용할 때 서버에 요청하는 request는 따로 클래스를 만들어 관리하기도 합니다.
이를 구현하는 방법 중 하나로 class 안에 통신 메서드들을 static 함수로 구현할 때 escaping이 유용합니다.
class Server {
static var values: [Value] = []
static getValues(completion: @escaping (Bool, [Value]) -> Void) {
//2. 서버로 request(요청)
Alamofire.request(urlRequest).responseJSON { response in
//3. response 전달 받음
persons.append("value")
DispatchQueue.main.async {
completion(true, values)
}
}
}
}
//1. getValues 실행 -> values를 요청
Server.getValues { (isSuccess, values) in
// 4. response에 따른 처리
if isSuccess {
}
}
위 코드를 예시로 이해해 봅시다.
서버에 value를 요청해서 response가 오면 적절한 처리를 해야합니다.
여기에서 생각해야 하는 것은 아래 두 가지 입니다.
- 요청과 처리가 비동기로 진행 돼야 한다.
- 3번 순서 후 4번 순서로 실행 돼야하는 것이 보장 되어야 한다.
만약 비동기로 처리가 되지 않으면 서버 통신이 오래 걸릴 때 앱이 멈출 수 있고,
3번 -> 4번 순서가 보장되지 않는다면 4번에서 원하는 동작이 안 되기 때문입니다.
그래서 위와 같은 경우에 escaping 클로저를 사용합니다.
위 코드는 escaping 클로저를 사용한 덕분에 아래 순서를 보장 받습니다.
- Server 클래스의 getValues(completion:)을 호출합니다.
- 서버에 Request를 전송하고 escaping 클로저인 { response in } 은 결과가 들어온 후 실행됩니다.
- responseJSON의 completionHandler가 실행되고 getValues(completion:)의 completion을 호출합니다.
- getValues(completion:)의 completion이 실행됩니다.
강한 순환 참조와 [weak self]
escaping 클로저를 이용할 때는 강한 순환 참조에 유의해야 합니다.
강환 순환 참조를 끊기 위해 자주 사용하는 테크닉인 [weak self]는 항상 사용해야 할까요?
아래에서는 어떨 때 [weak self]를 사용하면 좋은지, 언제 안 써도 되는지 알아보도록 합시다.
강한 순환 참조가 무엇인지는 아래 포스팅에서 확인해 주세요.
non-escaping 클로저일 때
non-escaping 클로저일 때는 [weak self]를 사용하지 않아도 됩니다.
non-escaping 클로저는
코드를 즉시 실행하고 함수 외부에 저장되거나 나중에 실행할 수 없기 때문에 순환 참조가 발생할 수 없습니다.
non-escaping 클로저라면 컴파일러가 메서드가 종료된 후 해당 클로저가 사용되지 않는다는 것을 확인합니다.
이는 인자로 전달한 클로저가 더 이상 메모리에 올라가 있지 않다는 것을 의미합니다.
따라서 순환 참조가 발생하지 않으므로 [weak self]를 사용할 필요가 없습니다.
고차 함수의 인자로 전달하는 클로저는 대표적인 non-escaping 클로저입니다.
따라서 고차 함수를 쓸 때는 [weak self]를 사용하지 않아도 됩니다!
DispatchQueue.main.asyncAfter(deadline:execute:)를 사용할 때
일정 시간 후에 코드를 실행하는 asyncAfter에서는
property를 저장하지 않는 한 [weak self]를 사용하지 않아도 됩니다.
public func asyncAfter(deadline: DispatchTime, qos: DispatchQoS = .unspecified, flags: DispatchWorkItemFlags = [], execute work: @escaping @convention(block) () -> Void)
asyncAfter는 인자 excute로서 escaping 클로저가 전달됩니다.
위와 같이 escaping 클로저가 인자로 전달되는 경우 해당 메서드가 종료된 후에도 메모리에 남아 있을 수 있기 때문에
순환 참조가 발생할 수 있습니다.
escaping 클로저이고 순환 참조 가능성이 있으므로 [weak self]를 사용해야 할까요?
정답은 아닙니다.
excute 클로저는 deadline으로 전달되는 시간동안 메모리에 유지됩니다.
deadline이 지나 클로저의 실행이 끝난 후에는 클로저가 메모리에서 제거되기 때문에 순환 참조가 발생하지 않습니다.
따라서 이 때는 [weak self]를 사용하지 않아도 됩니다.
같은 원리로 UIView.Animate, UIViewPropertyAnimator의 animation call 역시
property를 저장하지 않는 한 순환 참조의 위험이 없습니다.
[weak self]를 썼을 때와 안 썼을 때 차이
근데 이때는 [weak self]를 썼을 때와 안 썼을 때의 결과가 다릅니다.
class Class2 {
func format(_ value: Int) -> String { return String(value) }
func test() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
let formatted = self.format(10)
print("formatted: \(formatted as Any)")
}
}
deinit {
print("Class2 deinit")
}
}
class Class1 {
let class2: Class2
init() {
class2 = Class2()
}
func someEvent() {
class2.test()
}
deinit {
print("Class1 deinit")
}
}
var class1: Class1? = Class1() // 1
class1?.someEvent() // 2
class1 = nil // 3
이 코드는 class1에서 Class2 클래스의 인스턴스를 생성하고
someEvent()를 호출하면 Class2 클래스의 test()를 호출합니다.
제가 원하는 test()의 결과는 호출되고 1초 후에 10이 출력되기를 원하는 상황이에요.
[weak self]를 사용하지 않을 떄는 원하는대로 동작이 됩니다.
[weak self]를 사용하지 않을 때는 마지막 라인인 3에서 class1에 nil로 설정되도 강한 참조로 캡처가 되서
Class2의 Reference Count가 0이 되지 않아
1초 뒤에 10이 출력된 후 class2가 deinit 됩니다.
만약 여기에서 [weak self]를 사용하면 어떻게 될까요?
func test() {
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
let formatted = self?.format(10)
print("formatted: \(formatted as Any)")
}
}
test()를 [weak self]를 추가하여 실행해 보았습니다.
클로저에서 약한 참조로 캡처가 되어 rc가 증가하지 않습니다.
그래서 Class1이 deinit이 될 때 Class2도 즉시 deinit이 되고
1초 후에 nil이 출력되는 것입니다.
이렇게 메모리 leak 이유 뿐만 아니라 [weak self]를 쓰는 것에 따라 실행 결과가 달라질 수 있습니다.
따라서 귀찮다는 이유로 무조건 [weak self]를 쓰는 것은 주의해야 합니다.
함수 외부 변수에 저장될 때
함수 외부 변수에 escaping 클로저가 저장될 때는 [weak self]를 사용해야 합니다.
class Class2 {
var handler: ((Int) -> Void)? = nil
func format(_ value: Int) -> String { return String(value) }
func test() {
handler = { [weak self] value in
let formatted = self?.format(value)
print("formatted: \(formatted as Any)")
}
}
deinit {
print("Class2 deinit")
}
}
이번 코드에서는 test()에서 외부 변수인 handler에 클로저를 저장합니다.
만약 [weak self]를 사용하지 않는다면, 클로저에 대한 self 참조가 handler 변수에 의해 붙잡히게 되어
클로저는 self 인스턴스가 해제되는 것을 기다리고
self 인스턴스는 클로저가 해제될 때까지 기다리는 순환 참조가 발생하게 됩니다.
그래서 [weak self]를 사용하여 self를 캡처할 때 Reference Count를 증가시키지 않게 해서
self 인스턴스와 클로저 사이의 순환 참조를 끊어야 합니다.
guard let self = self else { return } vs self?.some
guard let self = self else { return } 와 self?.some 의 차이점에 대해 알아봅시다.
옵셔널 바인딩을 이용하는 경우 self가 nil이 아닌 경우 self를 지역 내에서 강한 참조하게 됩니다.
따라서 self에 대한 RC가 증가되었기 때문에 클로저가 증가될 때까지 self가 해제되지 않습니다.
옵셔널 체이닝을 이용하면 강한 참조를 하지 않기 때문에
클로저가 종료되기 전에 self가 해제됐을 때 불필요한 작업을 하지 않습니다.
그래서 클로저가 종료되기 전에 self가 해제되는 상황이라면 옵셔널 체이닝이 더 효율적이라고 할 수 있습니다.
추가) 클로저를 옵셔널로 감싸면?
파라미터로 넘기는 클로저를 옵셔널로 설정하면 어떻게 될까요?
non-escaping 클로저는 "함수 외부로 탈출할 수 없다"는 한계를 가지고 있습니다.
이 제한 사항은 "클로저"의 제한인 것이죠.
그래서 escaping을 붙여 함수 외부의 변수에 파라미터로 넘어온 클로저를 저장하거나, 함수가 종료된 후 실행했었죠.
하지만 파라미터가 옵셔널 타입의 클로저라면 이야기가 다릅니다.
파라미터를 옵셔널로 감쌌더니 오히려 escaping을 붙여 에러가 발생합니다.
escaping을 제거하면 에러가 사라지고요.
왜냐하면 옵셔널로 클로저를 감싸는 순간 클로저가 아닌 옵셔널 타입으로 변하기 때문에
클로저의 "함수 외부로 탈출할 수 없다"는 제한이 사라지기 때문입니다.
따라서 자동으로 escaping처럼 동작을 하기 때문에 escaping을 안 붙여도 되는 것이죠.
마무리
오늘은 escaping 클로저가 무엇인지에 대해 알아보았습니다.
관련하여 [weak self]를 사용해야 할 때와 사용하지 말아야 할 때에 대해서 알아보았고
지금까지 별 생각 없이 써오던 self 옵셔널 바인딩과 옵셔널 체이닝의 차이에 대해서도 알아보았습니다.
escaping 클로저여도 [weak self]를 안 써도 되는 상황이 있고
심지어 무조건 사용하면 결과가 달라져 위험할 수 있다는 것도 배웠네요.
self 옵셔널 바인딩, 옵셔널 체이닝의 차이도 몰랐는데 새로 알게 된 글이었습니다.
포스팅을 위해 공부하면서 새로 배운 내용이 많아 틀린 점이 있을 수도 있으니
만약 틀린 점이 있다면 댓글로 알려주세요!
감사합니다.
참고
https://www.youtube.com/watch?v=0sOrVoLOf7Q
https://velog.io/@haanwave/Article-You-dont-always-need-weak-self
https://docs.swift.org/swift-book/LanguageGuide/Closures.html
https://hcn1519.github.io/articles/2017-09/swift_escaping_closure
https://babbab2.tistory.com/164
아직은 초보 개발자입니다.
더 효율적인 코드 훈수 환영합니다!
공감과 댓글 부탁드립니다.