[SwiftUI][TCA] TCA Case Studies - 02. SharedStateFileStorage

별똥별·2025년 1월 31일

TCA

목록 보기
16/24

🚀 TCA에서 Shared 상태 관리하기: FileStorageKey 활용법

안녕하세요, 별똥별🌠입니다!
이번 글에서는 Composable Architecture(TCA)에서 Shared 상태를 관리하는 방법에 대해 살펴보겠습니다. 특히, @Shared 프로퍼티 래퍼를 활용하여 탭 간 상태를 공유하는 방법과, 데이터를 파일 저장소(FileStorageKey)에 영구적으로 저장하는 방법을 설명합니다. 🔥


🧐 Shared 상태란?

앱을 개발하다 보면 여러 화면에서 공유해야 하는 상태가 필요할 때가 있습니다.

예를 들어

  • 여러 탭(Tab)에서 동일한 데이터를 유지해야 할 떄
  • 특정 상태(예: 사용자 설정, 통계)를 앱이 종료된 후에도 보존해야 할 때
  • 특정 화면에서 데이터 변경 시, 다른 화면에서도 즉시 반영해야 할 때

이러한 문제를 해결하기 위해 TCA에서는 @Shared프로퍼티 래퍼를 제공합니다. 이를 사용하면 Reducer 간에 상태를 쉽게 공유할 수 있으며, FileStorageKey를 활용하면 앱이 종료된 후에도 데이터를 유지할 수 있습니다.


🎯 예제: Counter와 Profile에서 Shared State 활용하기

아래 코드는 Counter 탭에서 증가/감소한 숫자 상태를 Profile 탭에서도 확인할 수 있도록 하는 예제입니다. 또한, 앱을 종료해도 저장된 값을 유지합니다.

1️⃣ SharedStateFileStorage Reducer 정의

@Reducer
struct SharedStateFileStorage {
    enum Tab { case counter, profile }
    
    @ObservableState
    struct State: Equatable {
        var currentTab = Tab.counter
        var counter = CounterTab.State()
        var profile = ProfileTab.State()
    }
    
    enum Action {
        case counter(CounterTab.Action)
        case profile(ProfileTab.Action)
        case selectTab(Tab)
    }
    
    var body: some Reducer<State, Action> {
        Scope(state: \ .counter, action: \ .counter) {
            CounterTab()
        }
        Scope(state: \ .profile, action: \ .profile) {
            ProfileTab()
        }
        Reduce { state, action in
            switch action {
            case .counter, .profile:
                return .none
            case let .selectTab(tab):
                state.currentTab = tab
                return .none
            }
        }
    }
}

이 Reducer는 두 개의 서브 Reducer(CounterTabProfileTab)를 포함하며, 탭 간 상태 전환을 관리합니다.


2️⃣ CounterTab에서 @Shared 상태 활용하기

@Reducer
struct CounterTab {
    @ObservableState
    struct State: Equatable {
        @Presents var alert: AlertState<Action.Alert>?
        @Shared(.stats) var stats = Stats()
    }
    
    enum Action {
        case alert(PresentationAction<Alert>)
        case decrementButtonTapped
        case incrementButtonTapped
        case isPrimeButtonTapped
        
        enum Alert: Equatable {}
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .alert:
                return .none
                
            case .decrementButtonTapped:
                state.$stats.withLock { $0.decrement() }
                return .none
                
            case .incrementButtonTapped:
                state.$stats.withLock { $0.increment() }
                return .none
                
            case .isPrimeButtonTapped:
                state.alert = AlertState {
                    TextState(
                        isPrime(state.stats.count)
                        ? "👍 The number \(state.stats.count) is prime!"
                        : "👎 The number \(state.stats.count) is not prime :("
                    )
                }
                return .none
            }
        }
        .ifLet(\.$alert, action: \ .alert)
    }
}

@Shared(.stats)를 사용하여 Stats 객체를 공유합니다. incrementButtonTappeddecrementButtonTapped 액션이 호출되면, 공유된 Stats 인스턴스가 변경되며 ProfileTab에서도 즉시 반영됩니다.


3️⃣ ProfileTab에서 공유된 상태 활용하기

@Reducer
struct ProfileTab {
    @ObservableState
    struct State: Equatable {
        @Shared(.stats) var stats = Stats()
    }
    
    enum Action {
        case resetStatsButtonTapped
    }
    
    var body: some ReducerOf<Self> {
        Reduce { state, action in
            switch action {
            case .resetStatsButtonTapped:
                state.$stats.withLock { $0 = Stats() }
                return .none
            }
        }
    }
}

ProfileTab에서도 @Shared(.stats)를 사용하여 CounterTab과 동일한 Stats 상태를 공유합니다. 이로 인해 CounterTab에서 증가/감소된 값이 ProfileTab에서도 유지되며, Reset 버튼을 누르면 상태가 초기화됩니다.


💾 FileStorageKey로 데이터 영구 저장하기

TCA의 @Shared는 기본적으로 앱이 실행되는 동안에만 데이터를 유지합니다. 하지만 FileStorageKey를 활용하면 데이터를 파일에 저장하여 앱이 종료되어도 데이터를 유지할 수 있습니다.

📌 Stats를 파일 저장소에 저장

struct Stats: Codable, Hashable {
    private(set) var count = 0
    private(set) var maxCount = 0
    private(set) var minCount = 0
    private(set) var numberOfCounts = 0
    
    mutating func increment() {
        count += 1
        numberOfCounts += 1
        maxCount = max(maxCount, count)
    }
    
    mutating func decrement() {
        count -= 1
        numberOfCounts += 1
        minCount = min(minCount, count)
    }
}

extension SharedKey where Self == FileStorageKey<Stats> {
    fileprivate static var stats: Self {
        fileStorage(.documentsDirectory.appending(component: "stats.json"))
    }
}

이렇게 하면 Stats 객체가 stats.json 파일에 저장되어 앱이 재시작되어도 데이터를 유지할 수 있습니다! 🎉


✅ 정리

  • @Shared를 사용하면 여러 Reducer에서 상태를 공유할 수 있다.
  • FileStorageKey를 활용하면 앱이 종료되어도 데이터를 유지할 수 있다.
  • @Shared(.stats)를 사용하여 CounterTab과 ProfileTab에서 동일한 데이터를 사용할 수 있다.

이제 여러분도 TCA에서 Shared 상태를 활용하여 강력한 상태 관리를 구현해보세요! 🚀🔥
다음 글에서 더 흥미로운 TCA 활용 사례를 다뤄보겠습니다. 감사합니다! 😊

profile
밍밍

0개의 댓글