Starbucks Caramel Frappuccino
본문 바로가기
  • 그래 그렇게 조금씩
SwiftUI/SwiftUI(Basic)

[SwiftUI] PropertyWrapper, @State, @Binding

by Toughie 2023. 4. 13.

스스챌 에러를 해결하던 중.. @State와 @Binding을 사용하는 경우도 많았고, 
해당 개념이 있었다면 더 빠르게 해결 할 수 있었을 문제도 있어서 간단하게 정리해보려 한다.

 

Property wrapper

우선 프로퍼티 래퍼(@붙은 녀석들)가 무엇인지 알아보자.

'랩으로 감싼다' 프로퍼티를 특정 타입으로 감싸서 추가적인 동작이나 기능을 가진 프로퍼티를 선언할 때 쓰인다.

이 타입은 PropertyWrapper 프로토콜을 준수하고, 프로퍼티 읽기/쓰기 (get, set) 에서의 동작을 정의한다.

 

SwiftUI에서 Property wrapper는 데이터 상태 관리를 심플하게 하는데 자주 쓰인다.

대표적인 프로퍼티 래퍼가 @State, @Binding 이다.

 

@State

프로퍼티의 값이 변경될 때 뷰를 업데이트 할 수 있는 상태 변수를 선언하는데 사용됨.

https://developer.apple.com/documentation/swiftui/state

 

State | Apple Developer Documentation

A property wrapper type that can read and write a value managed by SwiftUI.

developer.apple.com

UIKit에서는 Property Observer를 통해 변화가 일어나면 뷰를 업데이트 했지만
SwiftUI에서는 @State 프로퍼티 래퍼를 통해 더 간단하게 뷰를 업데이트 할 수 있다.

 

@State로 선언된 변수의 값이 변할 때마다 View를 다시 그림!

 

@State변수는 '해당 뷰 내에서만' 사용되는 상태 변수이다.

만약 다른 뷰에서 직접 접근해 버리면 사이드이펙트가 발생할 수 있다.

그래서 보통 private 접근제어를 걸어준다. @State private var a: Int = 7 이런 식으로

다른 뷰에서 해당 @State 변수를 업데이트 하지 말고, @Binding을 사용해서 
다른 뷰에서 '간접적으로 업데이트' 하는 방식으로 처리한다.

 

그런데 SwiftUI에서 View는 구조체이다..

그리고 화면에 보이는 뷰인 body가 get-only 계산속성이다..

View가 참조타입이 아닌 값타입인데 어떤 방식으로 변경사항을 반영해서 뷰를 새로 그리는 것일까?

 

@State 변수는 Heap에 할당된다! 

View에는 @State 변수의 데이터가 할당되어 있는 메모리 주소를 가리키는 포인터만 있다.

그래서 새로운 뷰가 만들어져도(값타입_복사되어도) 해당 포인터는 여전히 Heap의 메모리 주소를 가리키고 있기 때문에

데이터의 변경 사항이 새로운 뷰에 적용 되는 것이다. 

 

@Binding

https://developer.apple.com/documentation/swiftui/binding

 

Binding | Apple Developer Documentation

A property wrapper type that can read and write a value owned by a source of truth.

developer.apple.com

바인딩은 데이터를 저장하는 프로퍼티와 데이터를 표시하고 변경하는 뷰 사이의 다리 역할을 한다.

음.. 설명만 보면 애매해서 공식문서의 예시 코드를 보자.

struct PlayButton: View {
    @Binding var isPlaying: Bool

    var body: some View {
        Button(isPlaying ? "Pause" : "Play") {
            isPlaying.toggle()
        }
    }
}

바인딩 변수는 타입선언만 해두었다. 직접 값을 할당해 초기화 하지 않았다.

즉 PlayButton 구조체에서 직접 바인딩 변수에 값을 할당하거나 변경하는 것이 아니라는 뜻.

 

버튼뷰를 다른 뷰에서 초기화 한다.

struct PlayerView: View {
    var episode: Episode
    @State private var isPlaying: Bool = false

    var body: some View {
        VStack {
            Text(episode.title)
                .foregroundStyle(isPlaying ? .primary : .secondary)
            PlayButton(isPlaying: $isPlaying) // Pass a binding.
        }
    }
}

body 부분에서 버튼뷰를 만들었는데 isPlaying의 값으로 $isPlaying을 전달하고 있다.

여기서 $isPlaying은 
@State private var isPlaying: Bool = false 요걸 바인딩으로 변환해 전달한다는 뜻

 

 

자 이제 버튼을 누르면 힙 영역에 저장되어 있는 PlayView의 @State 변수의 값이 변경될 것이고,
변경되니까 뷰도 다시 그려질 것이다.

 

"값을 뷰의 부모 뷰에서 전달받아 자식뷰에서 부모 뷰의 값을 업데이트 하는 것"

struct ParentView: View {
    @State private var count = 0

    var body: some View {
        ChildView(count: $count)
    }
}

struct ChildView: View {
    @Binding var count: Int

    var body: some View {
        Button(action: {
            count += 1
        }) {
            Text("Increment count")
        }
    }
}

이 코드를 보면 
ParentView가 count 변수를 가지고 있고 
ChildView가 count 변수를 업데이트하는 버튼을 가지고 있다.

즉 부모 뷰에서 선언한 변수를 자식 뷰에서 업데이트하는 것이다.

부모 - 자식 간의 연결고리 정도로 이해해 볼 수 있겠다.

 

 

사실 생각보다 바로 와닿지는 않는데.. 해당 프로퍼티 래퍼들에 대해 알아본 이유는
초반에 언급했던 것처럼 스스챌 코딩 중에 바인딩을 쓰지 않고 온통 @State변수만 사용하니까

부모 뷰와 자식 뷰가 상태변화에 따라 전부 업데이트를 하는 바람에 의도와는 다른 결과가 많이 나왔다.

 

(배경뷰의 애니메이션은 그대로 유지되어야 하는데, 부모 뷰에서 어떤 액션을 취하면
부모 뷰의 @State변수에 따라 뷰가 업데이트되면서 배경에도 영향이 간다든지..)

요약하면 부모 뷰에서 직접 값을 세팅하고 변경해야 하는 코드가 있다면,
자식 뷰에서는 해당 변수를 @State가 아닌 @Binding으로 선언해서 해결된 문제가 많았다는 것이다.

사실 중요한 건 부모 뷰의 업데이트인데 자식뷰들까지 계속 상태변화에 따라 업데이트를 했던 것...

좀 더 많이 써보고 설계도 해보고 해야 직접적인 개념이 더 잘 와닿고 활용도도 높아질 것 같다.
스스챌 에러를 수정하면서  부모-자식 뷰에 대해 다시 한번 고민할 수 있게 되었고,
@State, @Binding 프로퍼티 래퍼에 대한 개념과 예시를 알아볼 수 있어서 좋았다.

 

우선 에러 해결해서 만족 ㅎ...

프로퍼티 래퍼는 공부 더 깊게 해야 할 듯 ㅎ...