-
SwiftUI : List 업데이트가 안됨 ( Swift 랜더링 최적화 과정 )Apple🍎/SwiftUI 2025. 4. 9. 18:05
"왜 안 바뀌는 거야?"
메인 화면인 HomeView에서는 활동 목록이 쭉 나오고, 거기서 특정 활동을 선택하면 DetailView로 이동해 상세 정보를 보고 편집할 수 있었습니다. 문제는 DetailView에서 활동 정보를 수정하고 저장한 다음에 HomeView로 돌아왔을 때 발생했어요. 분명히 CoreData를 통해 뷰 모델에는 데이터 변경 사항이 반영됐는데, 화면의 리스트는 업데이트 되지 않고 이전 상태를 계속 가지고 있더라고요.
코드는 이런 식이었어요
// 데이터 변경 후 viewModel.loadData() // 새로운 데이터를 불러오는 함수
분명히 데이터를 새로 불러와서 @Published var todayActivities: [Activity] = []에 새 배열을 할당했는데도 말이죠. 로그를 찍어보니 데이터는 확실히 변경되었는데 화면은 그대로였어요. ( 아니 배열도 값 타입을 따르니까 교체를 했으면 새 값으로 인식이 되어야 하잖아.?? 왜 @Published 된 값의 변경 사항이 생겼는데 화면이 렌더링이 안되냐고~~~~ )
이것저것 다 시도해봤던 발버둥
처음에는 SwiftUI가 뭔가 잘 작동을 못하나(Poor~~) 하고이런저런 트릭을 다 시도해봤어요.
- View 초기화 강제로 다시 하기
.onAppear { // View가 나타날 때마다 강제로 데이터 로드 viewModel.loadData() }
- State 변수 활용해서 강제 렌더링
@State private var refreshToggle = false // 데이터 로드 후 self.refreshToggle.toggle() // State 변경으로 View 강제 업데이트 시도
- viewModel에서 objectWillChange로 해당 뷰 모델을 참조하는 View 업데이트 스케쥴링 하기
objectWillChage.send()
objectWillChange.send( ) 동작
ObservableObject 프로토콜을 채택한 클래스(보통 ViewModel들)는 자동으로 objectWillChange라는 퍼블리셔를 가지게 됩니다.
이건 Combine 프레임워크의 PassthroughSubject인데, 이 퍼블리셔는 객체의 @Published 속성이 변경되기 직전에 이벤트를 발송하는 역할을 합니다. 보통은 @Published 속성을 변경하면 SwiftUI가 자동으로 objectWillChange.send()를 호출해주지만, 수동으로 호출하는 경우 다음과 같습니다.
- "이봐들 다들 주목해! 나 지금 바뀌려고 해!"라는 신호를 보냅니다.
- 이 뷰 모델을 @ObservedObject, @StateObject, 또는 EnvironmentObject로 관찰하고 있는 모든 뷰들이 이 신호를 받습니다.
- 신호를 받은 뷰들은 "오, 내 데이터가 바뀌네? 나도 업데이트해야겠다"라고 생각하고 UI 업데이트를 스케줄링합니다.
뷰를 강제로 업데이트하기 위해 별에별 시도를 다 해봤지만 모두 소용이 없었어요.ㅠㅠ 도대체 왜 다시 그리질 않냐고??????/
왜 UUID만으로는 부족했나?
List { ForEach(viewModel.todayActivities) { activity in // Activity는 Identifiable을 채택하고 있어서 기본적으로 id 속성을 사용 ActivityCard(activity: activity) // 기타 설정들... } }
Activity 모델은 Identifiable 프로토콜을 채택하고 있어서 ForEach 문 안에서 UUID인 id 속성을 자동으로 식별자로 사용하고 있었죠. 여기서 중요한 포인트는, 활동의 내용은 바뀌어도 UUID는 그대로라는 점이에요.
그러니까 상황을 정리해보면
- 상세 화면에서 활동의 제목이나 메모를 수정했어요.
- 이 정보는 데이터베이스에 저장됐고요.
- HomeView로 돌아와서 viewModel.loadData()를 호출해 새 데이터를 불러왔어요.
- viewModel.todayActivities에는 분명 수정된 정보가 담긴 새 배열이 할당됐어요.
- 하지만 SwiftUI는 이 배열의 각 항목을 UUID로 식별하고 있었고...
- UUID는 그대로니까 "아, 이 항목은 이미 화면에 있네. 변경 없음!" 하고 판단했던 거죠.
- 그 결과 UI는 업데이트되지 않았어요.
로그로 찍어보면 분명 데이터는 바뀌었는데, 화면은 그대로였던 이유가 바로 이거였어요.
이때 저는 SwiftUI가 어떤 기준으로 뷰를 업데이트하는지 완전히 이해하지 못했었거든요. 값 타입인 배열 totalActivites를 통째로 바꿨으니까 당연히 뷰도 다시 그려질 거라고 생각했죠. 하지만 SwiftUI는 더 똑똑(?)해서 각 항목의 식별자를 기준으로 뷰를 업데이트할지 말지 결정해요. 식별자가 그대로면 "이미 있는 항목이니까 다시 그릴 필요 없어"라고 판단하는 거죠.
복합 식별자의 발견
SwiftUI에게 "이건 같은 활동이지만, 상태가 바뀌었어!" 라고 알려줄 방법이 필요했어요. 그래서 생각해낸 해결책이 바로 UUID와 수정 시간을 합친 복합 식별자였죠.
ForEach(viewModel.todayActivities) { activity in ActivityCard(activity: activity) // 기타 설정들... } .id(activity.id.uuidString + "\(activity.modifiedAt.timeIntervalSince1970)")
이렇게 하니까 활동이 수정될 때마다.
- modifiedAt 시간이 업데이트되고
- 이로 인해 전체 식별자 문자열이 변경되고
- SwiftUI는 "아, 이건 새로운 항목이구나!" 하고 인식해서 뷰를 다시 그리게 되죠
이 한 줄을 추가하자마자 모든 게 제대로 작동하기 시작했어요! 😂
배운 점들
- SwiftUI는 식별자를 정말 중요하게 여겨요: SwiftUI가 뷰를 업데이트할지 말지 결정하는 핵심 기준이 바로 식별자예요.
- 상태 변경 = 식별자 변경이어야 해요: 내용이 바뀌면 식별자도 함께 바뀌어야 SwiftUI가 제대로 반응해요.
- 타임스탬프는 최고의 친구예요: 수정 시간은 상태 변경을 추적하는 데 정말 유용해요. 거의 모든 모델에 updatedAt 같은 필드가 있으니 활용하세요!
- SwiftUI의 최적화를 이해해야 해요: SwiftUI는 성능을 위해 최적화하는데, 때로는 이게 직관과 다르게 작동할 수 있어요.
이전까지 SwiftUI가 뷰 업데이트를 식별자를 기준으로 한다는 사실을 알고 있었으나 @Published 된 속성 값 자체가 변경이 되어도 이를 인식하기보다 그저 id 식별자를 기준으로 뷰 업데이트를 판단한다는 사실을 알게되었어요.
다른 해결책들도 있을까?
- ForEach에 id: \.self 사용하기: 전체 객체를 식별자로 사용하면 속성이 바뀔 때 식별자도 바뀌어요. 하지만 이건 성능에 영향을 줄 수 있어요.
ForEach(viewModel.todayActivities, id: \.self) { activity in // ... }
- 리스트 자체에 랜덤 ID 할당하기: 매번 업데이트할 때마다 리스트 전체에 새 ID를 주는 거예요.
List { // ... } .id(UUID()) // 매우 극단적인 방법! 리스트가 항상 처음부터 다시 그려져요
하지만 이런 방법들은 각각 단점이 있어요. 첫 번째는 모든 속성이 Hashable해야 하고, 두 번째는 너무 비효율적이죠. 저희의 복합 식별자 방식이 가장 균형 잡힌 해결책이라고 생각해요.
'Apple🍎 > SwiftUI' 카테고리의 다른 글
SwiftUI의 View 생명주기와 상태 관리: iOS와 macOS의 차이점 상세 분석 (0) 2025.04.10 SwiftUI 멀티 플렛폼 Navigation 아키텍쳐 설계 (Coordinator 야 저리 가라) (0) 2025.04.08 [Issue] SwiftUI: Canvas 컴포넌트가 onAppear로 인한 @State 값 변경을 업데이트 하지 않음 (0) 2025.03.24 List와 ScrollView+LazyVStack 비교하기 (0) 2025.03.07 SwiftUI 데이터 모델에 actor를 사용하면 안되는 이유 (0) 2025.02.19