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 과 함께 count 가 0 이 되는구나. 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초 후에 실행되게 된다. 하지만 인스턴스가 해제되었으므로 self 는 nil 이 되기때문에 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!"
}
}
}
참고 자료
아주아주 정말 고마운 분이시다. 이분의 강의를 많이 참고하여 재해석 하였다.
'→ Swift Study' 카테고리의 다른 글
[Swift] @State, @Binding? (1) | 2024.10.23 |
---|---|
[Swift] Do, Try, Catch 간단하게 알아보기 (0) | 2024.02.09 |
[Swift] GCD 알아보기 4 - 동시성 관련 문제 (0) | 2024.01.11 |
[Swift] GCD 알아보기 3 - DispatchGroup (0) | 2024.01.11 |
[Swift] GCD 알아보기 2 - GCD 종류 (0) | 2024.01.09 |