[SwiftUI] body 프로퍼티 내에서 왜 데이터 수정이 안 되는 걸까요? (feat. View Protocol, @State)

Deah (김준희)·2024년 9월 6일
0

SwiftUI

목록 보기
1/1
post-thumbnail

안녕하세요. Deah 입니다.

저는 지난 달부터 조금씩 SwiftUI를 공부해보고 있는데요!
오늘은 간단히 SwiftUI에서 사용하는 body 프로퍼티에 대해서 알아보려고 합니다.

View Protocol

SwiftUI는 구조체 기반으로 구현되어 있기 때문에 공통적인 기능을 위해 Protocol을 많이 사용합니다.
위 사진은 SwiftUI로 프로젝트를 구성했을 때 초기 화면인데
자세히 보시면 ContentView라고 선언된 구조체 옆으로 View 프로토콜을 채택하고 있는 걸 볼 수 있어요!


View
A type that represents part of your app’s user interface and provides modifiers that you use to configure views.

공식 문서에서는 View 프로토콜은 유저 인터페이스의 일부를 나타내며, View를 구성하는 수정자(modifiers)를 제공한다고 설명되어 있습니다.

View 프로토콜의 내부를 살펴보면 associatedtype으로 Body를 구성하도록 강제되어 있고,
Body도 View 프로토콜을 채택하고 있네요 🤓 (some에 대해서는 추후 더 자세히 다뤄볼게용)


body

body
The content and behavior of the view.

body는 View의 실직적인 내용과 로직을 담고 있는 부분이에요.
Required Default~ 문구를 통해 필수로 정의되어야 하는 부분이구나 알 수 있습니다.


변수 수정해보기

그럼 이제 테스트를 시작해보죵!

import SwiftUI

struct ContentView: View {
    var name = "Deah"
    
    var body: some View {
        Button {
            name = "Kim"
        } label: {
            Image(systemName: "star")
        }

    }
}

View 프로토콜을 채택한 구조체를 정의하고 star 버튼을 눌렀을 때 name이 수정하도록 처리되어있는 코드입니다.
이 코드는 정상적으로 동작할까요?


아니요 🙅‍

🚨 Cannot assign to property: 'self' is immutable

self가 immutable 하기 때문에 프로퍼티를 변경할 수 없다는 에러메세지를 발견하실 수 있습니다.

아니 var 변수로 선언되었는데 왜 변경할 수 없냐고 생각할 수 있겠죠? (제 얘기입니다.)
이 문제를 이해하기 위해서는 Struct, 즉 구조체에 대해서 살펴보아야 합니다.


Struct

struct YearCountTest {
    
    var year = 2024
    
    var thisYear: String {
        return "올해는 \(year)년 입니다."
    }
    
    var nextYear: String {
        year += 1
        return "내년은 \(year)년 입니다."
    }
    
}

이 코드에서는 어디에서 오류가 발생할지 예상되시나요?

역시 year 프로퍼티를 수정하는 부분에서 오류가 발생합니다.

왜죠?

올해와 내년을 알려주는 문자열을 리턴하는thisYear, nextYear연산 프로퍼티(Computed Property)인데요

struct YearCountTest {
    var year = 2024
    
    var thisYear: String {
        get {
            return "내년은 \(year)년 입니다."
        }
        
        set {
            // 필요없을 땐 생략 가능!
        }
    }
}

연산 프로퍼티는 원래 get과 set을 작성해주어야 하지만, set이 필요없을 때 get은 생략할 수 있습니다.
그리고 get의 기본 특성은 non-mutating(불변)이기 때문에 nextYear 연산 프로퍼티에서 year 변수를 수정할 수 없었던 것이죵.

mutating

🧐 : 그럼 평생 못바꾸나요?
🧑‍🏫 : 가능합니다만

get 내에서 변수를 변경하고 싶을 경우 mutating 키워드를 사용해 해결할 수 있습니다.

struct YearCountTest {
    var year = 2024
    
    var thisYear: String {
        return "올해는 \(year)년 입니다."
    }
    
    var nextYear: String {
        mutating get {
            year = year + 1
            return "내년은 \(year)년 입니다."
        }
    }
}

변수 year를 변경하고 싶던 연산 프로퍼티 nextYear를 mutating 키워드를 사용해 get을 작성해주면 더이상 오류가 발생하지 않습니다.

깨끗하죠?


🧐 : 연산 프로퍼티에서만 가능한가요?
🧑‍🏫 : 메서드에서도 가능합니다만

구조체 내에서 메서드를 정의했을 때에도 mutating 키워드가 없다면 변수를 변경할 수 없지만
mutating을 붙여주면 에러 없이 잘 실행됩니다 :)

struct YearCountTest 
    var year = 2024
    
    func changeYear() {
        year += 1
    }
    
    mutating func mutatingChangeYear() {
        year += 1
    }
}

body에 적용하기

앞서 SwiftUI는 구조체 기반으로 되어있고,
View 프로토콜로 강제되어 있는 body 프로퍼티는 연산 프로퍼티입니다.

그렇다면 body에서도 당연히 데이터를 변경할 수 없지 않을까요?

body의 공식 문서를 다시 한 번 살펴보면 이제는 잘 보이실 거 같습니다.
View 프로토콜 내에서 body는 mutating get이 아닌 get으로 강제되어 있습니다.

var body: Self.Body { get }

🧐 : 그럼 body를 mutating get으로 바꾸면 되지 않나요?

짜잔~! 빨간줄에 당첨되었어요!
body 연산 프로퍼티를 mutating get으로 변경하면 위와 같은 오류가 발생하게 됩니다.

🍎 : "너 ContentView에 View 프로토콜 따르지 않고 있는데? 수정하지 않을래?"

이유는 body 연산 프로퍼티를 mutating get으로 변경하는 순간 View 프로토콜에서 get으로 강제하고 있던 body 연산 프로퍼티가 없다고 간주하기 때문에 "너 View 프로토콜 채택했으면 body 넣어줘야지. 수정해!" 라고 알려주는 것입니다.


@State

🧐 : SwiftUI에서도 데이터를 수정하고 싶은데요?
🍎 : @State를 사용해

개발자는 당연히 프로그램 내에서 원활하게 데이터를 다루고 가공하고 싶어하겠죠?
하지만 구조체 기반으로 되어있는 SwiftUI에서는 지금까지 살펴보았듯 데이터를 변경하는게 불가능해요.
그래서 애플은 @State를 사용하라고 알려줍니다.


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

State는 SwiftUI에서 값(데이터)를 읽고 쓸 수 있는 Property Wrapper(프로퍼티 래퍼)입니다.


State의 내부 구조를 살펴보면 @PropertyWrapper로 감싸져있는 모습을 볼 수 있죠?

예전에 Property Wrapper를 통해 UserDefaultsManager를 다루는 포스팅을 한 적이 있는데
궁금하시면 한 번 보고 오셔도 좋을 거 같습니다 😶‍🌫️

@State 사용해보기

예제를 다시 가져와서 @State를 적용해 보겠습니다.

import SwiftUI

struct ContentView: View {
    
    @State private var name = "Deah"
    
    var body: some View {
        Button {
            name = "Kim"
            print("name >>>", name)
        } label: {
            Image(systemName: "star")
        }
        .onAppear {
            print("name >>>", name)
        }
    }
    
}

@State를 통해 프로퍼티를 선언해주고, onAppear 시점과 버튼을 클릭한 다음 시점에 각각 name을 프린트로 확인해보면 값이 잘 변경된 것을 확인할 수 있습니다.


마무리

오늘은 SwiftUI에서 body 프로퍼티 내부에서 데이터가 변경되지 않는 이유를 바탕으로 구조체의 특징과 PropertyWrapper까지 살짝 알아봤습니다.

다음 기회에 시간이 된다면 @State와 @Binding에 대해서 또는 some View 타입에 대해서 추가적으로 알아보면 좋을 거 같네요!

모쪼록 이 글이 저와 같이 스유를 처음 알아가시는 분께 도움이 되었으면 좋겠습니다.
잘못된 부분이 있다면 댓글로 피드백 부탁드립니다. 👻

안녕!!!!!!!

profile
기록 중독 개발자의 기록하는 습관

0개의 댓글