→ Swift Study

[Swift] 약한참조 (weak self) Swift 로 실습해보기

Swift librarian 2024. 1. 21. 02:21

Swift 를 사용하면서 본인도 모르게 self. 를 많이 사용했을 것이다. self 는 말그대로 나 자신! 이라는 뜻을 가지고 있다. 그렇다면 [weak self] 는 무엇일까? 실습을 통해 아주 간단하게 맛보기만 보려고 한다.

약한참조

간단하게 ARC 에 대해서 설명해 보자면, Swift에서는 ARC(Automatic Reference Counting, 자동 참조 카운팅)를 사용하여 객체의 메모리를 관리한다. 이 참조를 약한 참조(weak)로 만들면 참조 카운트가 증가하지 않는다. ARC 에 의해서 참조 카운팅이 0 이 되었을때 메모리에서 해제가 되는데 강한 참조가 걸려있으면 카운팅이 0 이 되기전까지 해제되지 않는다.

 

예시코드

기본 구조

아래같이 Navigate 버튼을 누르면 다음뷰로 가고 다음뷰는 Hello, World 문구가 있는 간단한 프로그램이다.

코드를 살펴보면 ViewModel 이 있고, 이 뷰모델이 시작될때 init 이 프린트되고, getText() 라는 함수가 실행된다. 뷰모델이 끝났을때 deinit 을 프린트하게 하는 코드를 작성했다.

struct HelloWorldView: View {
    @StateObject var vm = HelloWorldViewModel()
    
    var body: some View {
        VStack {
            if let text = vm.text {
                Text(text)
            } else {
                ProgressView()
                    .controlSize(.extraLarge)
            }
        }
        .font(.largeTitle)
    }
}

class HelloWorldViewModel: ObservableObject {
    @Published var text: String? = nil
    
    init(text: String? = nil) {
        print("init")
        getText()
    }
    
    deinit {
        print("deinit")
    }
    
    func getText() {
        self.text = "Hello, World!"
    }
}

 

Count 추가

여기서 아래와 같이 count 라는 변수를 초기값 0 으로 두어 init 과 함께 +1 을 해주고, deinit 과 함께 -1 을 해주게 했다.

    init(text: String? = nil) {
        print("init")
        count += 1
        getText()
    }
    
    deinit {
        print("deinit")
        count -= 1
    }

그렇게 된다면 당연히 init, deinit 이 반복되니 0, 1 이 반복되서 나올 것이다.

 

Hello, World! 가 3초후에 보인다면...?

아래와 같이 getText() 안의 명령들을 3초 후에 하게 한다면?

HelloWorldView 에는 아래와 같이 text 가 Hello, World! 가 아니라면 다른 뷰를 표시해주게 처리해줬다.

struct HelloWorldView: View {
    @StateObject var vm = HelloWorldViewModel()
    
    var body: some View {
        VStack {
            if let text = vm.text {
                Text(text)
            } else {
                ProgressView()
                    .controlSize(.extraLarge)
            }
        }
        .font(.largeTitle)
    }
}

결과는? 두구두구 아래와 같이 3초후에 Hello, World! 를 볼 수 있다. 또한 뒤로갔을때 deinit 까지 되면서 count 가 0 이 되는 모습도 확인할 수 있다.

여기서 궁금한점! 만약에 3초가 지나고 Hello, World! 가 나오기 전에 뒤로가면 어떻게 될까? 와우... 3초후에 deinit 과 함께 count0 이 되는구나. 3초가 지나야 메모리에서 해제가 되는구나! 정도로 이해하면 좋다.

 

Hello, World! 가 500초후에 보인다면...?

아래와 같이 Hello, World!500초가 지나야 보일 수 있게 해보았다. 

    func getText() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 500) {
            self.text = "Hello, World!"
        }
    }

그렇다면 결과는...? 아이고... init 만 되고 count 만 더해지고 deinit 이 되지않는 모습을 볼 수 있다... 여기서 우리는 [weak self] 를 사용해 볼 수 있다.

 

weak self 약한 참조 적용

아래와 같이 [weak self] 를 넣어준다면 어떻게 될까?

    func getText() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 500) { [weak self] in
            self?.text = "Hello, World!"
        }
    }

와... 500초가 지나지도 않았는데 deinit 이 되면서 count 가 다시 0 이 되는 것을 확인해 볼 수 있다.

deinit 이 되었다는 소리는 ViewModel 인스턴스가 메모리에서 해제되었다는 뜻이다. 하지만 getText() 라는 작업은 내가 이미 시켰다. 그렇다면 이것은 어떻게 되는 것일까? 이 일은 GCD에 의해 대기열(DispatchQueue)로 가서 500초 후에 실행되게 된다. 하지만 인스턴스가 해제되었으므로 selfnil 이 되기때문에 self?.text = "Hello, World! 는 불행하게도 실행되지는 않는다. 그렇기 때문에 self 뒤에 ? 를 붙이는 것이다. 옵셔널이기 때문에...

 

결론

[weak self] 를 제대로 이해하기 위해서는 ARC, GCD, 참조, 캡쳐 등 여러가지에 대한 종합적인 이해가 필요한 것 같다. (갈길이 멀다..) 하지만 이렇게 Swift 실습으로 [weak self] 가 어떤 친구인지 조금이나마 와닿길 바라며...

전체 코드

UserDefault 를 사용하여 count 를 관리하였다.

import SwiftUI

struct ContentView: View {
    @AppStorage("count") var count: Int?
    
    init() {
        count = 0
    }
    
    var body: some View {
        NavigationStack {
            VStack {
                NavigationLink("Navigate") {
                    HelloWorldView()
                }
                .font(.largeTitle)
                .buttonStyle(.bordered)
            }
        }
        .overlay(alignment: .topTrailing) {
            Text("\(count ?? 0)")
                .font(.system(size: 60))
                .bold()
                .padding(30)
        }
    }
}

struct HelloWorldView: View {
    @StateObject var vm = HelloWorldViewModel()
    
    var body: some View {
        VStack {
            if let text = vm.text {
                Text(text)
            } else {
                ProgressView()
                    .controlSize(.extraLarge)
            }
        }
        .font(.largeTitle)
    }
}

class HelloWorldViewModel: ObservableObject {
    @AppStorage("count") private var count = 0
    @Published var text: String? = nil
    
    init(text: String? = nil) {
        print("init")
        count += 1
        getText()
    }
    
    deinit {
        print("deinit")
        count -= 1
    }
    
    func getText() {
        DispatchQueue.main.asyncAfter(deadline: .now() + 500) { [weak self] in
            self?.text = "Hello, World!"
        }
    }
}

참고 자료

아주아주 정말 고마운 분이시다. 이분의 강의를 많이 참고하여 재해석 하였다.