Lifetime
1편에서는 SwiftUI가 View를 구분하는 방식인 Identity를 정리했다. 이번에는 Identity가 유지되는 시간인 Lifetime과, 그 안에서 State가 어떻게 보존되는지 살펴본다.
View Value는 계속 새로 만들어질 수 있지만, Identity가 유지되는 동안에는 같은 View의 Lifetime 안에 있다고 볼 수 있다. 그리고 State와 StateObject는 이 Lifetime에 연결되어 있기 때문에 Identity가 바뀌면 함께 교체된다.

고양이가 하루 동안 이곳저곳 움직이더라도 우리는 계속 같은 고양이라고 생각한다. 이처럼 Identity는 시간에 따라 달라지는 여러 값을 하나의 안정적인 존재로 연결한다. 시간의 흐름 속에서 연속성을 만들어 주는 셈이다.
View는 Lifetime 동안 여러 상태를 가진다. 각각의 상태는 서로 다른 값이지만, SwiftUI는 이 값들을 시간의 흐름 속에서 하나의 동일한 존재, 즉 하나의 View로 연결한다.
struct PurrDecibelView: View {
var intensity: Double
var body: some View {
// ...
}
}SwiftUI가 body를 실행할 때마다 새로운 View Value가 만들어진다.
고양이 데시벨 측정 예제
var body: some View {
PurrDecibelView(intensity: 25)
}처음에는 intensity가 25인 PurrDecibelView가 만들어진다. 이때 만들어진 값이 바로 View Value다.
var body: some View {
PurrDecibelView(intensity: 50)
}값을 50으로 바꾸면 body가 다시 호출되고 새로운 View Value가 만들어진다. 같은 View 정의에서 나온 값이지만, 두 값은 서로 다르다.
SwiftUI는 View가 바뀌었는지 확인하기 위해 이전 Value의 복사본을 잠시 보관하고 비교한다. 비교가 끝나면 그 Value는 사라진다.
여기서 기억해야 할 점은 View Value와 View Identity가 서로 다르다는 것이다. View Value는 일시적인 값이므로, View Value 자체의 Lifetime에 무언가를 의존해서는 안 된다.
그렇다면 우리가 신경 써야 하는 것은 View의 Identity다. SwiftUI는 1편에서 살펴본 여러 방법으로 View에 Identity를 부여한다. 앱이 실행되는 동안 값이 바뀌어 새로운 View Value가 만들어져도, 같은 Identity에 연결되어 있다면 이 값들은 모두 같은 View를 나타낸다.
View의 Identity가 바뀌거나 View가 제거되는 순간, 해당 View의 Lifetime도 끝난다.
A view’s lifetime is the duration of the identity.
State와 StateObject
View Identity와 Lifetime의 관계를 이해하면 SwiftUI가 상태를 유지하는 방식도 자연스럽게 이어진다. 이제 State와 StateObject를 살펴보자.
struct CatRecorder: View {
@State var title = ""
@StateObject var mic = Microphone ()
var body: some View {
VStack {
TextField ("Title:", text: $title)
MicView(mic)
}
}
}SwiftUI가 View에서 State나 StateObject를 발견하면, 해당 데이터를 View의 Lifetime 동안 유지해야 한다는 것을 알게 된다.
즉, State와 StateObject는 View의 Identity에 연결된 영속 저장소다. View가 처음 만들어질 때 SwiftUI는 State와 StateObject의 초기값을 사용해 메모리 안에 저장 공간을 할당한다.

위 캡처처럼 title이 "" -> "Miao" -> "Nocturnal miao"처럼 변하면 View Value는 새로 만들어지지만, Identity가 유지되므로 같은 저장 공간을 사용한다.
Identity 변경이 State에 미치는 영향
var body: some View {
if dayTime {
CatRecorder()
} else {
CatRecorder()
.nightTimeStyle()
}
}겉보기에는 같은 View가 조건문의 서로 다른 두 분기에 존재한다. 하지만 Structural Identity 때문에 SwiftUI는 이 두 View를 서로 다른 Identity를 가진 View로 판단한다.
처음 body를 실행했을 때, true 분기로 들어가면, SwiftUI는 State의 초기값을 사용하여 영속 저장 공간을 할당한다. 그 View의 Lifetime 동안 여러 동작에 의해 State가 변경되더라도 SwiftUI는 그 값을 계속 보존한다.
하지만 dayTime이 바뀌면 어떻게 될까? false 분기의 View는 다른 Identity를 가진 별개의 View다. 따라서 SwiftUI는 false 분기용 저장 공간을 새로 만들고 State의 초기값부터 다시 시작한다. true 분기에서 사용하던 저장 공간은 해제된다. 다시 true 분기로 돌아가도 새로운 View로 취급되므로 State의 초기값부터 시작한다.
즉, Identity가 바뀔 때마다 State도 교체된다.
데이터 기반 View와 ForEach
SwiftUI에는 데이터의 Identity를 View의 Explicit Identity로 사용하는 여러 데이터 기반 구조가 마련되어 있다. 대표적으로 ForEach가 있다.
가장 단순한 형태의 ForEach initializer는 고정된 범위를 받는다. 예를 들면 다음과 같다.
정적 데이터 컬렉션 예제
ForEach(0..<5) { offset in
Text("🐑 \(offset)")
}
이 범위 안의 offset을 사용해 ViewBuilder가 만든 각각의 View를 식별한다.
고정된 범위만 받도록 제한하면 해당 View의 Lifetime 동안 각 Identity가 안정적으로 유지된다는 것을 보장할 수 있다.
동적 데이터 컬렉션 예제
struct RescueCat {
// ...
var tagID: UUID // 여기서 Apple 공식은 Id가 아닌 ID임을 알 수 있다...
}
ForEach(rescueCats, id: \.tagID) { rescueCat in
ProfileView(rescueCat)
}이번에는 컬렉션과 함께 식별자 역할을 하는 프로퍼티의 KeyPath를 받는다.
이 프로퍼티는 Hashable해야 한다. SwiftUI가 컬렉션의 각 원소에서 만들어진 View에 Identity를 부여할 때 이 값을 사용하기 때문이다. 데이터에 안정적인 Identity를 제공하는 일이 중요한 이유다.
그래서 Swift 표준 라이브러리에는 이런 식별자를 제공하기 위한 Identifiable 프로토콜이 정의되어 있다.
struct RescueCat: Identifiable {
// ...
var tagID: UUID
var id: UUID { tageID }
}
ForEach(rescueCats) { rescueCat in
ProfileView(rescueCat)
}컬렉션의 원소가 Identifiable을 준수하면 KeyPath를 직접 넘기지 않아도 프로토콜이 제공하는 식별자로 데이터와 View의 Identity를 정의할 수 있다.
ForEach 이니셜라이저 정의
Swift에서는 타입 시스템을 이용해 데이터가 시간의 흐름 속에서도 같은 데이터인지 SwiftUI가 알 수 있게 한다.
extension ForEach where Content: View, Data.Element: Identifiable, ID == Data.Element.ID {
public init(
_ data: Data,
@ViewBuilder content: @escaping (Data.Element) -> Content
)
}이 경우 ForEach 이니셜라이저는 두 가지 값을 받는다.
- 제네릭 아규먼트
Data로 표현되는 컬렉션 - 컬렉션의 각 원소로부터
View를 생성하는 방법, 즉 escaping closure
컬렉션의 원소인 Data.Element가 Identifiable을 준수하도록 제한해서 SwiftUI가 데이터의 Lifetime 동안 해당 데이터를 계속 추적할 수 있게 한다.
이런 ForEach 기반 View는 우리가 제공한 데이터의 Identity를 이용해 데이터와 연결된 View의 Lifetime 범위를 결정한다. 따라서 좋은 Identifier를 선택하는 것은 View와 데이터의 Lifetime을 직접 제어하는 방법이기도 하다.
정리
View Value는 계속 만들어지고 사라지는 값이다. 반면 Identity는 시간의 흐름 속에서 View를 같은 존재로 이어 주고, 그 결과 Lifetime과 상태 저장소가 유지된다. 그래서 SwiftUI에서 상태 문제를 만났을 때는 @State만 보기보다 먼저 View의 Identity가 유지되고 있는지 확인해야 한다.
- View Value는 일시적이고, View Value의 Lifetime에 의존해서는 안 된다. Identity는 View가 시간의 흐름 속에서도 같은 존재로 이어지도록 연속성을 부여한다. (UIKit에서 이어져 온 상식을 파괴하자)
- View의 Identity를 직접 제어하면
State의 Lifetime이 어디서 시작되고 어디서 끝나는지도 명확하게 정할 수 있다. - 마지막으로 SwiftUI는 데이터 기반 컴포넌트에서
Identifiable프로토콜을 적극적으로 활용한다. View로 만들 데이터에는 반드시 안정적인 Identifier를 선택해야 한다. 데이터의 Identity가 흔들리면 그 데이터로부터 만들어지는 View의 Lifetime도 함께 흔들린다.