[SwiftUI] @Published vs @State

marisol👩🏻‍💻·2022년 7월 30일
1

이번에 SwiftUI와 MVVM으로 프로젝트를 진행하면서 Property Wrapper를 실제로 사용해보았는데, @Published와 @State의 개념이 헷갈려서 정리하려고 한다


@Published와 @State 개념이 헷갈렸던 이유는, 둘 다 해당 프로퍼티 래퍼로 선언되어 있는 프로퍼티의 값이 바뀔 때 뷰를 다시 그려준다고 생각했기 때문이다
공식문서와 예시를 보면서 차이점을 정리해보겠다

📝 @Published

@Published로 표시된 프로퍼티 값이 바뀌면 외부로 바뀐 값을 퍼블리시하는 타입이라고 정의되어 있다
@Published 속성으로 프로퍼티를 게시하면, 그 타입에 대한 퍼블리셔가 생성된다
그 퍼블리셔에 접근하려면 $사인을 붙여야하고, 공식문서 예제는 아래와 같다

class Weather {
	@Published var temperature: Double
    init(temperature: Double) {
    	self.temperature = temperature
    }
}

let weather = Weather(temperature: 20)
cancellable = weather.$temperature
	.sink() {
    	print("Temperature now: \($0)")
}
weather.temperature = 25

@Published로 선언된 프로퍼티 값이 변경되면, 그 프로퍼티의 WillSet 블럭에서 퍼블리싱이 발생한다.
=> 그 프로퍼티를 구독하고 있는 구독자들이 실제로 그 프로퍼티에 새로운 값이 세팅되기 전에 새로운 값을 전달 받게 된다는 뜻인 것 같다

위에 코드에서 sink()는 컴바인 메서드인데 @Published로 선언된 temperature값이 바뀔 때마다 호출된다고 한다
(곧 combine도 공부해야겠다..😇)

그래서 맨 처음에 Weather가 초기화되면서 temperature가 20으로 세팅되었을 때 20이 프린트되고, 두번째로 weather.temperature를 25로 바꿨을 때 또 25가 프린트 된다

// Prints:
// Temperature now: 20.0
// Temperature now: 25.0

그리고 중요한 것은,

The @Published attribute is class constrained. Use it with properties of classes, not with non-class types like structures.

@Published는 클래스의 프로퍼티에만 사용할 수 있고, structure 같은 non-class 타입에서는 사용이 제한된다고 한다

프로젝트를 진행할 때는 ViewModel이 옵저빙하고, 값이 바뀜에 따라 뷰가 다시 그려지도록 하기 위해서 ViewModel을 다 @ObservableObject를 채택하도록 해주었다.
그런데 이 @ObservableObject는 AnyObject를 채택하는 프로토콜이기 때문에 ViewModel을 class로 선언해주고, 내부 프로퍼티들을 @Published로 선언해주었다

final class ContentViewModel: ObservableObject {
	// 이 프로퍼티들의 값이 바뀌면 ContentView를 다시 그림
	@Published var isShowingSheet = false
    @Published var isShowingHistory = false
    ...
}

📝 @State

@State는 SwiftUI에 의해 관리되는 값을 읽거나 쓸 수 있는 프로퍼티 래퍼 타입이다

SwiftUI는 state로 선언된 프로퍼티의 저장소를 관리한다고 한다
그 프로퍼티의 값이 바뀌면, SwiftUI는 view hierarchy에서 그 프로퍼티 값에 의존하는 뷰들을 업데이트한다

그리고

Use state as the single source of truth for a given value stored in a view hierarchy.

라고 되어 있는데, single source of truth는
뷰에서 사용하는 데이터는 하나의 원천을 갖는다 또는
데이터가 여러 곳에 존재하지 않고 오직 한 곳에만 존재한다
라는 뜻으로 볼 수 있다고 한다

그래서 view hierarchy 중 여러 view에서 state로 선언된 프로퍼티들이 쓰이더라도, 데이터의 원천은 하나라고 생각하면 될 것 같다

만약 부모뷰가 자식뷰에게 state 프로퍼티를 전달하면, SwiftUI는 부모뷰의 state 프로퍼티 값이 바뀔 때마다 자식 뷰를 업데이트하겠지만, 자식 뷰는 그 값을 수정할 수는 없다
자식 뷰가 값을 변경하도록 하기 위해서는, Binding을 전달해주어야 한다

struct PlayerView: View {
	@State private var isPlaying: Bool = false
    
    var body: some View {
    	PlayButton(isPlaying: $isPlaying) // 자식뷰에게 binding 전달
    }
}

struct PlayButton: View {
	@Binding var isPlaying: Bool // 부모뷰에게서 binding 값 받음
    
    var body: some View {
    	Button(isPlaying ? "Pause" : "Play") {
        	isPlaying.toggle() // 부모뷰에게서 받은 값 수정 가능
        }
    }
}

그런데 위 상황에서 자식 뷰가 초기화 될 때 isPlaying도 초기화해버리면 SwiftUI의 storage management에 충돌이 일어난다 (다른 뷰더라도 isPlaying은 같은 값을 써야하기 때문인 것 같다)

그래서 부모뷰가 직접 자식뷰에게 state 프로퍼티를 공유하는 경우 이외에 다른 뷰에서 state 값에 접근하지 못하도록 state는 항상 private으로 선언하고, 그 값에 접근해야 하는 뷰들 중에 가장 상위의 뷰에 위치시켜야 한다
그리고 그 값에 접근해야 하는 자식뷰들은 그 값을 읽기 전용 (binding X) 또는 읽고 쓰기 전용 (binding O)으로 공유 받아서 사용해야 한다
그러면 state 프로퍼티들은 어떤 쓰레드에서도 안전하게 값을 바꿀 수 있다고 한다

정리하고 보니 쓰이는 곳이 확실히 다른 것 같다

우선 @Published는 class에서만 사용되고, @State는 struct 내의 프로퍼티의 값을 바꿀 때 사용한다 (그리고 @State는 private으로 선언해야함)
그리고 @Published는 class에서만 사용되기 때문에 struct 타입인 뷰에서는 사용하기 어려울 것 같고, @State는 SwiftUI에 정의되어 있기 때문에 뷰에서 사용하는 것이 적절해보인다


참고 자료

0개의 댓글