[SwiftUI] MVVM 패턴에서 데이터 값 변경하기

Page·2022년 6월 12일
0

SwiftUI

목록 보기
10/18
post-thumbnail
post-custom-banner

문제와 해결 과정

트위터 클론코딩을 하던 중 문제를 발견했다.

위 gif에서 좋아요가 동기화되지 않아 코드를 살펴봤다. ViewModel에서 데이터 값을 얻어 A View와 B View에 Observed로 전달하고 있었는데, 한쪽에서 데이터를 변경하면 다른 쪽 뷰에서는 뷰를 다시 그리지를 않아 생기는 문제였다. 강의자가 왜 이렇게 했는지 이해가 안됐다.

현재는 내가 나름대로 수정한 상태다. 거쳤던 과정을 한번 회고해보면

  1. Model에서 값을 변경해주기

MVVM 패턴을 찾아봤을 때 모델의 데이터 값 변경은 Model에서 이뤄져야 한다고 하기에 그렇게 했는데 여러 문제가 있었다. 일단 Model이 Struct로 만들어지는데 Struct에서는 함수로 데이터를 변경해줄 수 없다. Mutating을 이용하면 됐지만 다른 값들을 Firebase에서 가져와야 하는데 결국 스택에 저장되는 Struct는 이걸 이용할 수가 없었다. 클로저에서 값 변경을 해줘야하는데 Heap 영역에서 처리되지 않는 데이터들을 클로저에 넣어줄 수 없었다.

  1. Firebase에서 값 얻어 다른 변수에 저장하기

마찬가지로 Model 차원에서 해결하려던 시도였다. result라는 변수에 값을 저장하면 되지 않을까 했었는데 값 할당과 Firebase에서 데이터를 가져오는 일은 서로 다른 쓰레드에서 일어나는지 Result에 Firebase에서 값을 할당하기 전에 Model의 데이터에 Result 값이 먼저 할당되었다.

이쯤됐을 때 떠오른 생각은 ViewModel에서 Index를 이용해 처리하면 되는거 아니야? 하는 아이디어였는데 Tweet이 업데이트 되고 Index가 변경된다면 다른 문제가 생길 수도 있을거 같아서 일단 보류했다.

  1. inout으로 해결하기

MVVM 패턴을 더 찾아봤고 Model에서 값을 변경해주지 않는다, 라는 글을 찾았다. 그래서 이제는 ViewModel에서 데이터를 변경해주려 했는데 우선 Index를 이용하는 것은 배제하고 어떤 방법이 있을까 고려했다. inout을 이용해서 ViewModel의 함수 내에서 값을 처리해주려 했다.

Escaping closure captures 'inout' parameter 'tweet'

inout에 closure를 쓰면 안되는 모양이다. 결국 index로 문제를 해결했다.

  1. 각 View에 모델의 Index를 넘겨서 ViewModel에서 처리하기

A와 B View에 모두 index를 만들어주고 값을 넘겨서 처리한 방식이다. 좋아요 유무 문제뿐 아니라 전체 좋아요의 수까지 해결할 수 있었다. 이전 코드에서는 하트 버튼을 누르면 좋아요 수가 1 증가하는 식으로 동작했는데 실제 서비스로 만든다고 가정했을 때 다른 사람이 누른 좋아요까지 반영해야하기 때문에 좋아요 수가 2만큼 오를 수도, 10만큼 오를 수도 있는 문제다. 그래서 버튼을 누르면 다시 서버와 통신해서 변경된 값을 반영할 수 있었다.

	func likeTweet(idx: Int) {
        
        guard let uid = AuthViewModel.shared.userSession?.uid else { return }
        
        COLLECTION_TWEETS.document(tweets[idx].id).getDocument { snapshot, _ in
            guard let data = snapshot?.data() else { return }
            let tempTweet = Tweet(dictionary: data)
            self.tweets[idx].likes = tempTweet.likes + 1
            COLLECTION_TWEETS.document(self.tweets[idx].id).updateData(["likes": tempTweet.likes + 1]) { _ in
                COLLECTION_TWEETS.document(self.tweets[idx].id).collection("tweet-likes").document(uid).setData([:]) { _ in
                    COLLECTION_USERS.document(uid).collection("user-likes").document(self.tweets[idx].id).setData([:]) { _ in
                        
                    }
                }
            }
        }
    }

위는 ViewModel에 작성한 코드고 전체 코드를 올릴 필요는 없어보여서 일부만 잘랐다. 크게 어려울 점은 없고 뷰가 가지고 있는 Index값을 Parameter에 넣어서 데이터를 변경해주면 된다.

문제없이 동작했지만 그냥 마음에 들지 않았고, 다른 사람들은 SwiftUI에서 데이터를 업데이트 하기 위해 어떻게 하나 열심히 찾아보았다. 다른데서는 못 찾았고 결국 Apple Developer의 Tutorial에서 비슷한걸 찾았다.

그런데 여기서도 Index를 이용해서 처리해주고 있었다. 아무래도 Index로 값을 업데이트 해주는게 맞는거 같은데, 몇 달랐던 점만 짚어보려고 한다.

Apple Tutorial에서는?

내 코드와 다른 점은 크게 두가지였다. 한개는 내가 완전히 비효율적으로 접근했었다. 일단 코드부터 살펴보자.

struct LandMarkDetailView: View {
    
    @EnvironmentObject var landmarkViewModel: LandmarkViewModel
    var landmark: LandMarkModel
    
    var landmarkIndex: Int {
        landmarkViewModel.landmarks.firstIndex(where: { $0.id == landmark.id })!
    }
    
    var body: some View {
        ScrollView {
            VStack {
                MapView(coordinate: landmark.locationCoordinates)
                    .ignoresSafeArea(edges: .top)
                    .frame(height: 300)
                
                CircleImage(image: landmark.image)
                    .offset(y: -130)
                    .padding(.bottom, -130)
                
                VStack(alignment: .leading) {
                    
                    HStack {
                        Text(landmark.name)
                            .font(.title)
                        FavoriteButton(isSet: $landmarkViewModel.landmarks[landmarkIndex].isFavorite)
                    }

                    HStack {
                        Text(landmark.park)
                        Spacer()
                        Text(landmark.state)
                    }
                    .font(.subheadline)
                    .foregroundColor(.secondary)

                    Divider()

                    Text("About \(landmark.name)")
                        .font(.title2)
                    
                    Text(landmark.description)
                }
                .padding()

                Spacer()

            }
            .navigationTitle(landmark.name)
        .navigationBarTitleDisplayMode(.inline)
        }
    }
}
  1. 뷰에 데이터를 줄 때 Binding 시켜줄 필요가 없다.

위 LandMarkDevailView는 LandMarkRowView에서 네이버게이션 링크로 연결된 뷰인데 값을 @Binding으로 받지 않고 있다. 트위터 클론코딩에서 나는 B와 A에서 데이터를 변경하면 둘이 모두 뷰를 다시 그려야하니까 Binding을 해야겠구나! 하는 생각으로 각 뷰에 모두 Data를 바인딩해서 처리했다. 하지만 ViewModel에서 값이 변경이 된다면 어차피 이 변경된 값들이 들어갈 것이기 때문에 굳이 바인딩을 해줄 필요가 없었다. 당연히 데이터를 넘겨준 뷰에서는 ViewModel이 @Environment로 선언되어야한다. 그렇게 해준다면 데이터를 받는 뷰에서는 var로 해도 충분하다.

  1. Index 값을 얻는 부분.

나는 각 뷰에 Index 값을 전달했는데 Apple tutorial에서는 ViewModel의 배열에서 Id값이 같은 것을 찾아서 Index를 얻는다.

아이템이 정말 많다면 Index값을 찾는 시간 소요가 있을거라고 생각하는데 그렇게 걱정할만한 수준이 아니라면 이 방법도 좋아보인다. A -> B -> C -> D로 계속 인덱스를 전달해야 하는 상황이 있을수도 있는데 이런 귀찮음을 줄여볼 수 있는 솔루션이다.

내 코드에서는 Index 전달 과정을 보면
A View -> B View -> D View
A View -> C View -> D View
이런 과정으로 진행되고 있는데 Index를 넘기는 것을 잊는 상황이 꽤 빈번했다.

Reference

https://developer.apple.com/tutorials/swiftui/handling-user-input

post-custom-banner

0개의 댓글