iOS/Swift

swift. 중첩 클로져(nested closure)에서 weak self 사용?

StudySpare 2022. 4. 27. 18:43
반응형

 

 

weak self

 

weak self 는 순환 참조(strong reference cycle)를 방지하기 위해 사용한다. 일반적으로는 escaping closure에서 self를 사용할 때, 캡쳐리스트에 선언해두고 사용한다. escaping closure에서 self를 캡쳐하면 strong reference cycle(Automatic Reference Counting in Swift Docs)이 발생하기 쉽기 때문이다.

class Fetcher {
	var x: String?
    func callFetch() {
        self.fetchSomething { [weak self] error in
        	self?.x = "finished"
            print("error : \(error)")
        }
    }

    func fetchSomething(@escaping completion: (_: Error?) -> Void) {
        ...
        completion(nil)
        ...
    }
}

ref) Escaping Closures in Swift Docs

 

weak self 는 꼭 써야 할까?

closure 안에서 무조건 weak self를 써야 하는 건 아니다. 특히 non-escaping closure는 함수의 실행  scope내에서 실행되기 때문에 weak self를 쓸 필요가 없다. 

 

예제> escaping closure와 non-escaping closure에서 self.x에 접근하는 코드 (ref Escaping Closures in Swift Docs)

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

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

 

(사실 위 코드는 에러가 발생한다. escaping closure에서 self를 변경 시키고 있기 때문(mutate)이다.)

 

escaping closure는 함수의 실행 범위(scope)을 벗어나서 실행될 가능성이 있다. 해당 클로져가 실행되는 도중에 이를 실행시킨 인스턴스는 종료 될 수도 있다. 그런데 클로져 내에서 자신을 실행시킨 인스턴스를 강함참조로 잡고 접근하면, 크래시가 발생할 수 밖에 없다(retain cyle). 별도 thread로 분리되는 작업을 작성할 때는 순환 참조를 항상 신경써야 한다.

 

하지만 GCD에서는 [weak self]가 필수는 아니다. 하지만 상황에 따라 쓰는 게 안전 할 수 있다. 아래 stack overflow와 article을 참고할 수 있다.

GCD doesn't cause retain cycle

SHOULD WE USE [WEAK SELF] IN GCD

 

 

잠깐 샛길로 빠져보면....  2-3년전에  DispatchQueue.main.asyne{} 에서는 더이상 [weak self]를 선언해주지 않아도 된다는 발표(?)를 접했다. 그런데 기억이 나지 않아 서칭을 좀 했다. 오랜 휴직과 함께 기억도 날라간 건지 답답하기만 했는데, Swift 5.3에서 SE-0269가 반영되었다는 글을 찾았다. 자세한 내용은 읽어보면 좋을 것 같다.(샛길이므로 빠른 정리)

 

 

In SE-0269, adopted in Swift 5.3, they introduced a pattern where if you include self in the capture list, it eliminates the need for all the self references in the closure: (https://stackoverflow.com/questions/66542721/avoiding-using-self-in-the-dispatchqueue)

 

 

중첩 closure (nested closure)에서는 weak self를 어떻게 쓸까?

오늘 리서치의 시작은 이 질문이었다. 중첩 클로져에 모두 weak self를 넣어줘야 하는걸까? 아래와 같은 코드에서 fetchSomething 과 doSomething의 completion handler에서 모두 필요한지 의문이었다.

class Fetcher {
	var x: String?
    var doX: String?
    
    func callFetch() {
        self.fetchSomething { [weak self] error in // 여기도
        	self?.x = "finished"
            print("error : \(error)")
            
            //nested closure 
            self?.doSomething { [weak self] error in // 여기도 모두 weak self?
            	self?.doX = "finished"
            }
        }
    }

    func fetchSomething(@escaping completion: (_: Error?) -> Void) {
        ...
        completion(nil)
        ...
    }
    
    func doSomething(@escaping completion: (_: Error?) -> Void) {
        ...
        completion(nil)
        ...
    }
}

 

결론은 이렇다. weak self는 최상위 escaping closure에만 두면 된다. 상위 closure에서 이미 순환 참조를 끊었기 때문에 하위 closure에서 다시 순환 참조가 발생하지 않는다.

    func callFetch() {
        self.fetchSomething { [weak self] error in
        	self?.x = "finished"
            print("error : \(error)")
            
            //nested closure 
            self?.doSomething { error in 
            	self?.doX = "finished"
            }
        }
    }

 

 


위 내용은 retain count를 직접 계산해보면 더 잘 이해할 수 있다.  찾다보면 guard let self = self else { return } 을 사용한다던가 owned 캡쳐리스트를 사용한다던가 하는 다른 방식도 발견할 수 있다. 하지만 이 둘 모두 결국엔 참조가 발생하는 거라 예상치 못한 crash가 발생할 확률이 크다. 쓰임이 다해서 소멸하겠다는 아이(self)를 억지로 잡고 사용할 필요가 있는 코드인지 잘 생각해보면, 대부분 아닐 것이다. 그러니 weak self를 잘 사용해서 안전한 코드를 만드는 게 좋겠다.

반응형