iOS/SwiftUI

[SwiftUI] Demystify SwiftUI (2/3) - Lifetime

kimsangjunzzang 2025. 10. 29. 22:41

안녕하세요, iOS 개발하는 루피입니다.

 

오늘은 WWDC21 Demystify SwiftUI 영상을 정리하는 2번째 시간입니다. 저번 글에서는 SwiftUI가 뷰를 식별하는 방식을 이해했다면, 이번 글에서는 ID가 뷰와 데이터의 수명과 어떻게 연결되는지 정리해 보는 시간을 가져보도록 하겠습니다. 바로 시작합니다.

Lifetime

영상에서는 고양이 테세우스의 예시를 들고 있습니다. 이 고양이가 일어나서 밥을 먹던지, 시간이 지나 밖에서 나가서 산책을 하던지, 기분이 안좋아 저에게 화를 내더라도 똑같은 테세우스라는 것은 변합이 없습니다. 이러한 생각이 바로 IdentityLifetime을 연결하는 핵심입니다.

이전 글에서 봤던 위 앱도 동일합니다. 각각의 상태는 시점에 따라 다른 값을 가지게 됩니다. 코드를 통해 한번더 살펴보도록 하겠습니다.

struct PurrDecibelView: View {
    var intensity: Double

    var body: some View {
        // ...
    }
}

var body: some View {
    PurrDecibelView(intensity: 25)
}

위와 같은 코드가 있다고 생각해 봅시다. 그리고 이러한 코드가 시간이 지나 아래와 같은 값으로 변경 되었다고 생각해 봐요.

var body: some View {
    PurrDecibelView(intensity: 50)
}

이렇게 되면 SwiftUI는 값을 복사해 사본을 만들고 비교해 뷰가 변경되었는지를 확인하고 이후에 이전에 있던 값을 없애버립니다. 여기서 가장 중요한 핵심은 뷰 값이 뷰 ID와 다르다는 것입니다.


View Lifetime

뷰의 값은 일시적이지만 Lifetime에 의존해서는 안 됩니다. 그렇기에 우리가 통제할 수 있는 것은 Identity입니다.

위 그림처럼 View가 처음 onAppear로 생성되면, Identity를 가지게 되고 상태가 계속 변하게 됩니다. 이때, 자신의 Identity는 변하지 않습니다. 하지만, onDisappear로 뷰가 제거되거나 View의 Identity가 변경이 된다면, View Lifetime도 종료되게 되는 거죠.


State와 StateObject의 Lifetime 관계

State와 StateObject를 바탕으로 한번 더 살펴보도록 하겠습니다.

SwiftUI가 StateStateObject를 발견하면, 뷰의 Lifetime 동안 해당 데이터를 유지해야 한다는 것을 알게 됩니다. 즉, State와 StateObject는 뷰의 ID와 연결된 영구 저장소라는 것입니다.

뷰가 처음 Identity를 가지게 된다면, 초기값을 사용해 State 와 StateObject에 대한 메모리 저장소를 할당하고 이때부터 변화되는 값들을 지속적으로 관찰하게 되는 것이죠. 그렇다면 다른 코드도 봅시다.

위처럼 분기 처리를 하게 되면, 처음 진입할 때 dayTime 일 경우의 저장소를 만들게 됩니다. 그리고 만약 이러한 조건이 변경된다면 else 문에 해당하는 새로운 Identity를 갖는 View를 생성하게 되면, 이전에 만들어진 True에 대한 저장소는 바로 할당 해제가 되게 되는 것입니다. 만약 다시 True로 돌아가게 된다면, False일 때의 저장소를 해제하고 새로운 True일 때 저장소를 만들게 되겠죠? 즉, 정체성이 바뀔 때마다 상태가 바뀌게 됩니다.


ForEach에서 Identity의 역할

앞서 뷰의 상태와 ID, Lifetime에 대해 살펴봤다면, 이제 ForEach에서의 Identity가 왜 중요한지 자세히 이해할 필요가 있습니다.

위 사진처럼 ForEach를 이용해 생성된 모든 뷰에 ID를 할당하기 위해 값을 사용하므로 이 속성은 해시 가능해야 합니다.

ForEach를 사용할 때는 화면에 동적으로 여러 개의 뷰를 반복적으로 생성하게 되는데, 이때 SwiftUI는 각 뷰에 Identity를 할당해야만, 데이터 변경 시 올바르게 애니메이션 하고, 상태가 꼬이지 않게 유지할 수 있습니다. 예를 들면, 정수 범위로 반복하는 경우엔 오프셋을 활용하고, 데이터 컬렉션을 사용할 땐 각 아이템의 고유한 id를 기준으로 하는 것이죠.

그리고 영상에서는 id 값이 안정적이고 유일해야 함을 강조하고 있습니다. 왜 그럴까요?

  • 데이터가 삽입/삭제/이동될 때 SwiftUI가 각 아이템의 상태와 애니메이션을 올바로 매칭하기 위해.
  • 만약 id가 불안정(랜덤, 인덱스 기반)하다면, 데이터가 바뀔 때 모든 뷰가 refresh 되기에, UX와 성능이 떨어짐.
  • DB의 primary key나 영속되는 고유값을 id로 써야 한다는 실전 원칙이 생긴다.

SwiftUI의 Identifiable 프로토콜 또는 keyPath 방식은 이런 안정성을 보장하기 위한 도구입니다.


ForEach의 내부

  • data : collection을 의미합니다.
  • content : 컬렉션의 각 요소에서 뷰를 생성하는 방법을 의미합니다.

여기서 가장 흥미로운 내용을 Data, Element를 Identifiale 한 것으로 제한하고 있다는 것입니다.


정리

결국 이번 글 Lifetime에 대한 정리를 요약하게 된다면, 아래와 같습니다.

1. 뷰 값은 일시적이다.

SwiftUI에서 View는 struct(값 타입)이기 때문에, 화면에 보이는 뷰의 값은 매우 짧은 시간 동안만 존재하며, body가 재계산될 때마다 항상 새로운 값으로 다시 생성되며, SwiftUI는 각 시점의 값을 잠깐 가지고 비교만 합니다.

2. 뷰의 Lifetime는 identity의 지속 시간이다.

뷰의 진짜 지속 시간은 값이 아니라, “이 뷰가 같은 identity인가??”에 달려 있습니다. 즉, 뷰가 처음 생성되면 identity가 부여되고, 그 identity가 유지되는 한 그동안의 상태(State, StateObject 등)도 함께 살아 있게 되는 겁니다. 그리고 identity가 바뀌거나 뷰가 없어지면 lifetime도 끝나게 되는 거죠.

3. 상태(State)의 지속성은 lifetime에 묶여 있다.

예를 들어, State와 StateObject는 해당 뷰의 identity가 살아있는 동안만 메모리에 저장됩니다. identity가 변경, 분기, 제거되는 순간 저장소가 교체, 할당 해제되는 거죠.

4. 데이터에는 반드시 “Stable 하고 Unique 한 id”를 제공해야 한다.

랜덤값, 인덱스 기반 id는 금방 바뀌기 때문에 뷰가 전체 재생성되고 state가 리셋되는 버그가 생길 수 있기에 Identifiable protocol이나 keyPath로 항상 “stable identity”를 보장하는 구조를 선택해야 합니다.


오늘도 화이팅입니다.