iOS/Swift

[Swift] didSet과 값 타입, 참조 타입의 인스턴스

Dev.Andy 2023. 8. 22. 23:59

머리말

글을 쓴 이유

내가 쓴 코드

//
//  VideoViewController.swift

class VideoViewController: UIViewController {
    
    // 검색창과 테이블 뷰가 존재 -> searchBar에 didSet을 하면 왜 안될까?
    @IBOutlet weak var searchBar: UISearchBar!
    @IBOutlet weak var videoTableView: UITableView!
    
    // 검색 결과를 담은 videoList의 값이 변할 때마다 테이블 뷰를 업데이트
    var videoList = [Video]() {
        didSet {
            videoTableView.reloadData()
        }
    }
    
    // 검색 결과(API 요청/응답)에 따라 videoList 배열에 새로 결괏값을 추가하는 코드...
}

멘토님의 질문

저번 주에 위의 코드를 다듬고 있을 때 멘토님께서 searchBar에서 didSet을 적용하면 videoList에서 didSet을 적용했을 때에 비해 테이블 뷰가 바뀌지 않는 이유를 물어보셨다. 이에 대답을 잘하지 못했고 이 부분을 한번 정리해 보면 좋겠다 하셔서 이렇게 포스팅을 하게 되었다. (드디어 포스팅을...)

Property Observers와 didSet

Property Observers (프로퍼티 옵저버; 속성 감시자)의 정의

공식 문서에 정의된 Property Observers는 아래와 같다.

Property observers observe and respond to changes in a property’s value. Property observers are called every time a property’s value is set, even if the new value is the same as the property’s current value.
프로퍼티 옵저버(속성 감시자)는 프로퍼티의 값 변화를 관찰하고 이에 반응한다. 프로퍼티 옵저버는 프로퍼티의 값이 설정이 될 때마다 호출이 되는데, 심지어 새로운 값이 현재의 값과 같다 하더라도 호출된다.

Property Observers - Properties | Documentation

쉽게 말하면, 프로퍼티의 값이 변화하는 관철하여 "시점"에 따라 이에 반응하는 게 Property Observers이다. 시점은 크게 과거형과 미래형으로 두 가지로 나뉜다. 과거형은 didSet (설정되었다)이고 미래형은 willSet (설정 될 것이다)이다. 우리는 여기서 과거형인 didSet만 다루어 볼 것이다.

didSet의 정의

이어서 didSet에 대한 공식 문서의 정의는 아래와 같다. 과거형 did가 들어간 것에서 알 수 있듯이, 값이 설정 되자마자 호출이 되는 걸 확인할 수 있다.

didSet is called immediately after the new value is stored.
didSet은 새로운 값이 저장된 직후 호출된다.

예시 코드 실습

아래처럼 구조체와 클래스를 정의하고 이에 대한 인스턴스를 각각 할당하여, 해당 인스턴스에 didSet을 적용해 보았다.

구조체(값 타입)의 인스턴스에 대한 didSet

값 타입(value type) 구조체에서는 인스턴스의 실제 데이터가 스택에 저장된다. 따라서 프로퍼티의 값을 변경하면 새로운 프로퍼티 값을 가진 새로운 인스턴스가 기존의 인스턴스를 대체하기(replace) 때문에 didSet이 호출된다.

// 개 구조체 생성
struct Dog {
    var speed = 7
}

// 강아지라는 개 구조체의 인스턴스를 할당 및 didSet을 정의
var doggy = Dog() {
    didSet {
        print("CHANGED STRUCT INSTANCE")
    }
}

// 새 프로퍼티를 가진 새 인스턴스로 바뀌어 didSet이 호출됨
doggy.speed = 9 // CHANGED STRUCT INSTANCE

클래스(참조 타입)의 인스턴스에 대한 didSet

반면에 참조 타입(reference type)인 클래스에서는 스택 영역에는 클래스(Cat)의 주솟값이, 힙 영역에는 인스턴스(kitty)에 대한 실제 데이터가 저장이 된다. 따라서 아무리 프로퍼티의 값을 변경하더라도, 스택 영역에는 주솟값밖에 없기 때문에 인스턴스가 바뀌는 게 아니라 프로퍼티의 값만 변경이 된다.

// 고양이 클래스 생성
class Cat {
    var speed = 2
}

// 키티라는 고양의 클래스의 인스턴스를 할당 및 didSet 정의
var kitty = Cat() {
    didSet {
        print("CHANGED CLASS INSTANCE")
    }
}

// 인스턴스 프로퍼티의 값이 바뀌어도 인스턴스 자체가 변한 게 아니기에 didSet이 호출되지 않음
kitty.speed = 4 // (속도 프로퍼티의 값이 바뀌었음에도, print()가 호출되지 않음)

// 새로운 인스턴스를 재할당하면 주솟값이 변경되기에 didSet이 동작함
kitty = Cat() // CHANGED CLASS INSTANCE
kitty.speed // 2

클래스 인스턴스에서 didSet을 호출하고 싶다면 위처럼 변수에 클래스 인스턴스를 재할당해야 한다.

클래스 인스턴스에 대한 didSet을 역이용하기

한번 할당되어 변경되지 않는 클래스 인스턴스의 didSet

따라서 해당 블로그의 맨위의 코드를 아래처럼 변경하더라도 UISearchBar 클래스의 인스턴스인 searchBar가 변하지 않기 때문에 처음 할당이 될 때만 호출이 될 뿐, 다시 인스턴스 자체를 재할당하지 않는 이상 didSet이 호출되지 않는다.

//
//  VideoViewController.swift

class VideoViewController: UIViewController {
    
    // UISearchBar 클래스의 인스턴스인 searchBar가 변하지 않기에 didSet이 호출되지 않음
    @IBOutlet weak var searchBar: UISearchBar! {
        didSet {
            videoTableView.reloadData() // 테이블 뷰가 첫 한번 이후 다시 호출되지 않기에 의미가 없다
    }
    @IBOutlet weak var videoTableView: UITableView!
    
    // 검색 결과를 담을 videoList 배열을 초기화
    var videoList = [Video]()
    
    // 검색 결과(API 요청/응답)에 따라 videoList 배열에 새로 결괏값을 추가하는 코드...
}

didSet이 한번만 호출되는 걸 역이용한다면?

viewDidLoad()에 들어갈 부분을 줄일 수 있다

오히려 이를 역이용하여 viewDidLoad()에 들어갈 함수를 아래처럼 클래스 인스턴스의 didSet에 담을 수 있다.

//
//  VideoViewController.swift

import UIKit

class VideoViewController: UIViewController {
    
    @IBOutlet private weak var searchBar: UISearchBar! {
        didSet {
            searchBar.delegate = self
        }
    }
    
    @IBOutlet private weak var videoTableView: UITableView! {
        didSet {
            videoTableView.delegate = self
            videoTableView.dataSource = self
            videoTableView.prefetchDataSource = self
            videoTableView.rowHeight = 140
        }
    }
    
    var videoList = [Video]() {
        didSet {
            videoTableView.reloadData()
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

꼬리말

멘토님의 피드백

반쪽짜리 프로퍼티 옵저버

멘토님들께 위의 코드에 대한 의견을 구해 보았는데. 값의 변경에 반응하는 didSet의 기능을 활용하지 못하는 점이 아쉽다는 의견이 다수였다.

Void를 리턴하는 함수나 클로저 구문이라는 대안책

사실 기존에는 Void를 리턴하는 함수를 새로 선언하여 해당 함수에 원하는 할당을 한번에 담았다. 하지만 해당 함수가 너무 길어져서 보기 안 좋아서 위처럼 실행한 것인데, 아직 배우지는 않았지만 클로저를 활용하여 코드 베이스로 충분히 구현할 수 있다고 한다.

우선 연습 차원에서 위처럼 코드를 써 보도록 하자.