본문 바로가기

iOS

TCA 뷰 컴포넌트 만들기 - Store vs ViewStore 그리고 @ObservedObject

TCA를 공부하면서 SeoulSalam을 개발하다보니, 공부한 것을 적용하는 데에 초점을 맞춰서 코드가 깔끔하지 않고 반복되며 재사용성이 굉장히 낮아졌었다. 이러한 이유로 리팩토링을 하며 모듈화 작업을 진행하다가 뷰 구조체 안에서 메인뷰의 store을 어떻게 받아와서 사용해야 하는 지 공부한 결과, 이를 위해서는 Store와 ViewStore의 차이점이나 언제 @ObservedObject를 사용해야 하는지에 대한 명확한 이해가 필요하다는 것을 알았고 학습 후에 코드에 적용해 보았다.

 

Store와 ViewStore 

 

 

TCA에서, Store는 앱의 상태와 액션을 관리하는 컨테이너 역할을 하고 reducer가 액션에 해당하는 함수로 상태 변경을 한다. 이 개념 안에서 뷰에서 Store의 인스턴스를 생성해주고 WithViewStore로 해당 Store을 불러와주는 정도로 간단하게 생각하고 코드를 작성했었다. 하지만 ViewStore는 Store의 뷰 역할로, 관찰 가능한 상태를 통해 뷰와 상호 작용하며 일종의 '뷰모델'의 역할이었던 것이다. 따라서 TCA의 공식문서를 찾아보면 ObservableObject 프로토콜을 따르는 것을 확인할 수 있다!!! 따라서 이는 SwiftUI의 @ObservedObject 프로퍼티 래퍼와 함께 사용할 수 있다는 것이었다. 이를 통해 SwiftUI 뷰는 사용자 인터페이스를 렌더링하고 사용자 입력을 앱의 액션으로 변환하는 데 필요한 것을 얻어오는 것이었다.

 

 

@ObservedObject의 사용 

 

@ObservedObject는 SwiftUI에서 ObservableObject를 관찰하고 해당 객체가 변경될 때 뷰를 업데이트하는 데 사용되는 프로퍼티 래퍼이다. 앞서 말했듯, ViewStore가 ObservableObject를 따르기 때문에 @ObservedObject로 사용할 수 있다. 이를 통해, SwiftUI 뷰는 ViewStore 안의 상태의 변경을 감지하고, 변경이 발생할 때마다 자동으로 업데이트됩니다. 이 개념에 따라서 원래 메인뷰에 전부 다 작성되었던 레스토랑의 리스트를 보여주는 뷰를 따로 빼주었다.

 

// 레스토랑 목록 컴포넌트
struct RestaurantList: View {
    @ObservedObject var viewStore: ViewStore<RestaurantState, RestaurantAction>
    let certifiedState: String
    
    var body: some View {
        ScrollView {
            LazyVStack(alignment: .leading) {
                Section {
                    LazyVStack(alignment: .leading){
                        ForEach(viewStore.state.filteredRestaurants, id: \.self) { restaurant in
                            if restaurant.certifiedState == self.certifiedState {
                                NavigationLink(
                                    destination: RestaurantDetailView(store: .init(
                                        initialState: .init(restaurant: restaurant),
                                        reducer: restaurantDetailReducer,
                                        environment: .init(client: .live, mainQueue: .main.eraseToAnyScheduler())
                                    )
                                    ),
                                    label: {
                                        RestaurantCell(restaurant: restaurant)
                                    }
                                )
                            }
                        }
                    }.padding(.bottom, 80)
                }
            }.animation(.easeIn)
        }.frame(maxWidth: .infinity, maxHeight: .infinity)
    }
}

 

위의 코드에서, RestaurantList 뷰는 viewStore를 @ObservedObject로 선언한다. 이는 ViewStore의 상태 변경을 감지하고, 변경이 발생할 때마다 뷰를 업데이트하도록 한다. 

 

리팩토링과 모듈화 

 

위와 같은 코드처럼 ViewStore와 @ObservedObject을 활용하여 반복되는 코드 블록을 재사용 가능한 뷰 컴포넌트로 리팩토링을 할 수 있게 되었다. 이제 메인 뷰에서는 위의 컴포넌트를 이렇게 불러올 수 있을 것이다.

 

VStack{
    SearchField(store: store)
    RestaurantList(store: store, certifiedState: "Halal Certified")
    RestaurantList(store: store, certifiedState: "Self Certified")
    RestaurantList(store: store, certifiedState: "Muslim Friendly")
}

 

원래의 너저분한 코드에 비해서 훨씬 보기 좋아졌다. 이처럼, 리팩토링과 모듈화를 통해 코드의 효율성을 크게 향상시킬 수 있었다.