
안녕하세요, 별똥별🌠입니다!
이번 글에서는 Composable Architecture(TCA)에서 Shared 상태를 관리하는 방법에 대해 살펴보겠습니다. 특히, @Shared 프로퍼티 래퍼를 활용하여 탭 간 상태를 공유하는 방법과, 데이터를 파일 저장소(FileStorageKey)에 영구적으로 저장하는 방법을 설명합니다. 🔥
앱을 개발하다 보면 여러 화면에서 공유해야 하는 상태가 필요할 때가 있습니다.
예를 들어
- 여러 탭(Tab)에서 동일한 데이터를 유지해야 할 떄
- 특정 상태(예: 사용자 설정, 통계)를 앱이 종료된 후에도 보존해야 할 때
- 특정 화면에서 데이터 변경 시, 다른 화면에서도 즉시 반영해야 할 때
이러한 문제를 해결하기 위해 TCA에서는 @Shared프로퍼티 래퍼를 제공합니다. 이를 사용하면 Reducer 간에 상태를 쉽게 공유할 수 있으며, FileStorageKey를 활용하면 앱이 종료된 후에도 데이터를 유지할 수 있습니다.
아래 코드는 Counter 탭에서 증가/감소한 숫자 상태를 Profile 탭에서도 확인할 수 있도록 하는 예제입니다. 또한, 앱을 종료해도 저장된 값을 유지합니다.
@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(CounterTab과 ProfileTab)를 포함하며, 탭 간 상태 전환을 관리합니다.
@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 객체를 공유합니다. incrementButtonTapped 및 decrementButtonTapped 액션이 호출되면, 공유된 Stats 인스턴스가 변경되며 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 버튼을 누르면 상태가 초기화됩니다.
TCA의 @Shared는 기본적으로 앱이 실행되는 동안에만 데이터를 유지합니다. 하지만 FileStorageKey를 활용하면 데이터를 파일에 저장하여 앱이 종료되어도 데이터를 유지할 수 있습니다.
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 활용 사례를 다뤄보겠습니다. 감사합니다! 😊