궁금한 내용을 검색해보세요!
이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.
서근 개발노트
티스토리에 팔로잉
SWIFT/Grammar

Swift : 기초문법 [프로퍼티#3 - 프로퍼티 옵저버(감시자) - didSet, willSet]

서근
QUOTE THE DAY

-
Written by SeogunSEOGUN

반응형

본 게시글은 yagom님과Zedd0202님의 게시글을 참고하여 작성되었습니다.

 

 

프로퍼티 옵저버

  • 프로퍼티 옵저버는 값이 변경되면 작업을 취할 수 있음. 
  • 프로퍼티 값이 변경될 때마다 호출. 변경되는 값이 현재 값과 같더라도 호출됨
  • 상속받은 저장 프로퍼티 또는 연산 프로퍼티에도 적용됨
  • 상속받지 않은 연산 프로퍼티에는 사용 불가(사용할 필요도 없음) : gettersetter을 통해 감시자를 구현할 수 있기 때문
  • willSet : 값이 변경되기 직전에 호출하는 메서드
  • didSet : 메서드와 프로퍼티 값이 변경된 직후에 호출하는 메서드
프로퍼티 옵저버를 정의해서 프로퍼티 값의 변경을 모니터링 할 수 있다. 프로퍼티 옵저버는 자신이 정의한 저장 프로퍼티에 추가 할 수 있으며, super class(부모 클래스)를 상속한 프로퍼티에도 추가 할 수 있다. 

 

lazy 저장 프로퍼티를 제외한, 정의된 저장 프로퍼티에 프로퍼티 옵저버를 추가할 수 있다.

 

또한, 하위 클래스 내의 프로퍼티를 재정의하여, 상속된 프로퍼티(저장 프로퍼티 또는 연산 프로퍼티)에도 프로퍼티 옵저버를 추가할 수 있다. 하지만 상속받지 않은 연산 프로퍼티에는 사용 불가능하다.

 

오버라이드(override)되지 않은 연산 프로퍼티에 대한 프로퍼티 옵저버는 연산 프로퍼티 setter에서 해당 값의 변경을 관찰하고 이에 응답할 수 있으므로 정의할 필요 없다.

 

TIP
 
 

부모 클래스를 상속하는 하위 클래스
원래 저장프로퍼티에서만 프로퍼티 옵저버를 추가할 수 있지만, 부모 클래스를 상속하는 하위 클래스 프로퍼티는 저장 프로퍼티든 연산 프로퍼티든 프로퍼티 옵저버를 추가할 수 있다.

 

Swift에서는 프로퍼티에 get, set, didSet, willSet을 사용할 수 있다. 즉 get, set 세트이고 didSet, willSet이 세트이다. 하나의 프로퍼티에 get, set, didSet, willSet모두 함께 사용하지 못함.

 

willSet에서는 새 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 newValue를 사용.

didSet에서는 바뀌기 전의 값의 파라미터명을 지정할 수 있는데, 지정하지 않으면 기본 값으로 oldValue를 사용.

TIP
 
 

oldValue와 didSet
감시자 코드 블록 내부에서 oldValue 키워드를 사용하지 않거나 매개변수(didSet(oldValue))를 적어주지 않으면 didSet이 실행되지 않음

 

우선 struct를 이용한 didSet을 사용해보자면 (Struct에 대해 알아보려면 여기를 클릭해주세요.)

struct Progress {
    var task: String
    var amount: Int
}

이제 위 구조체의 인스턴스를 만들고 시간 경과에 따라 프로그래스를 조정할 수 있다.

var progress = Progress(task: "데이터 로딩", amount: 8)
progress.amount = 30
progress.amount = 80
progress.amount = 100

그리고 amount가 변경될 때마다 메시지를 출력되는 작업을 수행하려고 하는데, 여기서 didSet 속성 관찰자를 사용할 수 있다. 금액이 변경될 때마다 일부 코드가 실행된다.

struct Progress {
    var task: String
    var amount: Int {
        didSet {
            print("\(task)까지 \(amount)% 완료 되었습니다.")
        }
    }
}
var progress = Progress(task: "데이터 로딩", amount: 8)
progress.amount = 30
progress.amount = 80
progress.amount = 100

/*
데이터 로딩까지 30% 완료 되었습니다.
데이터 로딩까지 80% 완료 되었습니다.
데이터 로딩까지 100% 완료 되었습니다.
*/

또한 속성이 변경되기 전에 willSet을 사용하여 작업을 수행할 수 있지만 WillSet은 거의 사용되지 않는다.

 

첫 번째 예제

class Person {
    var nickName: String = "서근" {
        willSet {
            print("닉네임이 '\(nickName)'에서 '\(newValue)'으로 변경될 예정입니다.")
        }
        
        didSet {
            print("닉네임이 '\(nickName)'에서 '\(oldValue)'으로 변경되었습니다.")
        }
    }
}

let myNickName: Person = Person()
myNickName.nickName = "철수"

//닉네임이 '서근'에서 '철수'으로 변경될 예정입니다.
//닉네임이 '철수'에서 '서근'으로 변경되었습니다.

두 번째 예제

class StepCounter {
    
    var totalSteps: Int = 1000 {  //저장 프로퍼티
        //프로퍼티 옵저버
        willSet {
            print("totalSteps을 \(totalSteps)에서 \(newValue)로 설정하려고 합니다")
            
        }
        didSet {
            if totalSteps > oldValue  {
                print("\(totalSteps - oldValue)걸음이 추가되었습니다.")              
            }
        }
    }
}

let myStepCounter: StepCounter = StepCounter()
//willSet은 값이 저장되기 직전에 호출
//totalSteps을 1000에서 10000로 설정하려고 합니다
myStepCounter.totalSteps = 10000
//didSet은 값이 저장된 직후에 호출
//9000걸음이 추가되었습니다.

위 코드는 만보기 예제인데

 

totalSteps가 저장 프로퍼티이고 이것이 ⭐️ oldValue 부분이 되며, willSet의 새로운 인스턴스에서 totalSteps의 값을 10,000으로 저장해줬으니 이곳이 ⭐️ newValue이다. 그렇기 때문에 새로운 값이 저장된 직후에 호출되는 didSet 부분이 호출된다.

 

즉, 위에서 totalSteps를 처음에 1,000으로 초기화해줬으니 oldValue10,000이 아니라 1,000이고, 반대로 newValue10,000이 아니라 1,000이다.

 

프로퍼티 옵저버는 언제 사용?

Swift의 프로퍼티 옵저버를 사용하면 속성이 변경되기 전이나 후에 실행될 기능을 각각 willSetdidSet를 사용하여 연결할 수 있다.. 하지만 대부분의 경우 프로퍼티 옵저버는 쓰지 않는다. 그냥 가지고 있으면 좋을 뿐!

 

일반적으로 속성을 업데이트 한 다음 코드에서 직접 함수를 호출할 수 있다. 그럼 왜 귀찮게 그것을 사용해야 하고, 언제 그것을 사용할까?

 

프로퍼티 옵저버를 이용하면 가장 좋은 점은 편의성이다. 프로퍼티 옵저버를 사용하면 속성이 변경될 때마다 기능이 실행된다. 속성이 변경될 때마다 함수를 호출해야 한다는 것을 기억해야 한다. 그리고 만약 잊어버리면 코드 안에 찾기 어려운 버그가 있을 것이다.

 

반면에, didSet을 이용하여 기능을 속성에 직접 붙이면, 언제든 잘 실행된다!


프로퍼티 옵저버를 이용하면 안 좋은 점은, 느린 작업을 수행하는 didSet 속성 관찰자를 첨부하면 갑자기 단일 정수를 변경하는데 예상보다 시간이 오래 걸릴 수 있으며 문제가 발생할 수 있다.

didSet 대신 willSet을 언제 사용?

간단하게 말해서 질문에 따라 달라진다.

- 속성이 변경되기 전에 알고 싶나요?

- 아니면 변경 후에 알고 싶나요?

답은, 대부분의 경우 didSet 을 사용한다. 변경 후 작업을 수행하여 사용자 인터페이스를 업데이트하거나 변경 내용을 저장하거나 기타 작업을 수행할 수 있다. 그렇다고 해서 willSet 이 유용하지 않다는 뜻은 아니며, 다른 것들 보단 훨씬 덜 인기가 있을 뿐이다 :)

willSet 은 속성이 변경되기 전에 프로그램 상태를 알아야 할 때 사용된다.

 

예를 들어 SwiftUI는 일부 위치에서 willSet 을 사용하여 애니메이션을 처리하므로 변경 전에 사용자 인터페이스의 스냅숏을 생성할 수 있다. 이전 스냅숏과 이후 스냅숏이 모두 있는 경우 두 스냅숏을 비교하여 업데이트해야 하는 사용자 인터페이스의 모든 부분을 볼 수 있는 것이다!

struct BankAccount {
	var name: String
	var isMillionnaire = false
	var balance: Int {
		didSet {
			if balance > 1_000_000 {
				isMillionnaire = true
			} else {
				isMillionnaire = false
			}
		}
	}
}
struct App {
	var name: String
	var isOnSale: Bool {
		didSet {
			if isOnSale {
				print("Go and download my app!")
			} else {
				print("Maybe download it later.")
			}
		}
	}
}
struct FishTank {
	var capacity: Int
	var fishCount: Int {
		didSet {
			if fishCount > capacity {
				print("You have too many fish!")
			}
		}
	}
}

잘못된 예시

열거형enum 에는 사용하지 못한다.

enum Student {
	var name: String
	var debt: Int {
		didSet {
			if debt < 5_000 {
				print("This is great!")
			} else if debt < 20_000 {
				print("This is OK.")
			} else {
				print("Can I fake my own death?")
			}
		}
	}
}

 

프로퍼티 옵저버 TEST :  문제를 풀려면 이곳을 클릭해주세요.

 

전역변수와 지역변수

연산 프로퍼티와 프로퍼티 옵저버는 전역변수와 지역변수 모두에 사용 가능하다. 

 

전역변수는  함수나 메서드, 클로저, 클래스, 구조체, 열거형 등의 범위 안에 포함되지 않았던 변수나 상수, 즉 우리가 프로퍼티를 다루기 전에 계속해서 사용했던 변수와 상수는 모두 전역변수 전역상수에 해당된다.

 

지역변수는 함수 등 일정 영역에서만 사용할 수 있는 형태입. 즉 함수, 메서드 또는 closure context 내부에서 정의되는 변수.

 

지금 까지 사용해왔던 변수 상수는 모두 전역변수 또는 전역상수에 해당된다. 

 

전역 변수의 장점은 함수에서 불러와서 사용하게 될 경우 원래의 값이 변하는 거기 때문에 따로 전달을 해주거나 할 필요가 없다는 것이고, 지역 변수는 함수 내에서 특정 동작을 하고 그 값을 다른 곳으로 전달해 주거나 그냥 없어지거나 하는 경우에 사용된다. 쉽게말해 지역변수는 특정 상황에서만 잠깐 쓰이고 사라지는 변수다 라고 알고있으면 쉽다.

 

그리고 이전에 보던 전역변수와 지역변수는 모두 저장변수(stored variable) 이다. 저장 프로퍼티와 같은 저장변수는 값을 저장하는 역할을 하고 해당 값을 설정 및 검색할 수 있도록 해준다.

 

하지만 전역또는 지역변수를 연산변수를 정의할 수 있고, 저장변수를 위한 옵저버도 정의 가능하다.

 

전역 상수 및 변수는 지연 저장 프로퍼티처럼 처음 접근 시 항상 지연 연산이 이루어 지기 때문에 lazy 키워드를 사용해 연산을 늦출 필요가 없기에 lazy 키워드를 사용할 필요가 없다. 

 

반대로 지역 상수 및 변수는 절대로 지연 연산이 되지 않는다.

 

정리해보자면

  • 전역상수 및 변수는 은 항상 지연 연산된다. 즉, 필요할 때만 초기화.
  • 전역상수 및 변수는 연산 프로퍼티와는 다르게 lazy 키워드가 필요 없다.
  • 지역상수 및 지역변수는 지연 연산이 되지 않는다.

 

class Account { 
    var wonInPocket: Int = 1000 {  //저장 프로퍼티
        willSet {
            print("나의 지갑의 돈이 \(wonInPocket)에서 \(newValue)원으로 변경될 예정입니다.")
        }
        didSet {
            print("나의 지갑의 돈이 \(oldValue)에서 \(wonInPocket)원으로 변경되었습니다.")
        }
    }
    
    var dollarInPocket: Double { //연산 프로퍼티
        get {
            return Double(wonInPocket)
        }
        set {
            wonInPocket = Int(newValue * 1000)
            print("주머니의 달러를 \(newValue)달러로 변경중입니다.")
        }
    }
}

//나의 지갑의 돈이 1000에서 5500원으로 변경될 예정입니다.
//나의 지갑의 돈이 1000에서 5500원으로 변경되었습니다.
let myAccount: Account = Account()
myAccount.dollarInPocket = 5.5
//주머니의 달러를 5.5달러로 변경중입니다.

 

읽어주셔서 감사합니다🤟

 

 


잘못된 내용이 있으면 언제든 피드백 부탁드립니다.


서근


위처럼 이미지 와 함께 댓글을 작성할 수 있습니다.