안녕하세요, 별똥별🌠입니다!
이번 글에서는 Composable Architecture(TCA)에서 Shared 상태를 관리하는 방법을 살펴보겠습니다. 특히, @Shared 프로퍼티 래퍼를 활용하여 여러 화면에서 데이터를 공유하는 방법과, InMemoryKey를 사용하여 앱이 실행되는 동안만 데이터를 유지하는 방식을 설명합니다. 🔥
앱을 개발하다 보면 여러 화면에서 동일한 데이터를 유지해야 하는 경우가 있습니다.
예를 들어
- 여러 탭(Tab)에서 같은 데이터를 유지해야 할 때
- 특정 화면에서 데이터가 변경되면 다른 화면에서도 즉시 반영되어야 할 때
이러한 문제를 해결하기 위해 TCA에서는 @Shared 프로퍼티 래퍼를 제공합니다. 이를 활용하면 Reducer 간에 상태를 쉽게 공유할 수 있으며, InMemoryKey를 사용하면 앱이 실행되는 동안만 데이터를 유지할 수 있습니다.
아래 코드는 Counter 탭에서 증가/감소한 숫자 상태를 Profile 탭에서도 확인할 수 있도록 하는 예제입니다. 단, 앱을 종료하면 데이터가 사라지는 방식입니다.
@Reducer
struct SharedStateInMemory {
enum Tab { case counter, profile }
@ObservableState
struct State: Equatable {
var currentTab = Tab.counter
var counter = CounterTab.State()
var profile = ProfileTab.State()
}
enum Action: Sendable {
case counter(CounterTab.Action)
case profile(ProfileTab.Action)
case selectTab(Tab)
}
var body: some ReducerOf<Self> {
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: Sendable {
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: Sendable {
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 버튼을 누르면 상태가 초기화됩니다.
| 비교 항목 | InMemoryKey | FileStorage |
|---|---|---|
| 데이터 유지 범위 | 앱 실행중 | 앱이 종료된 후에도 유지 |
| 저장 위치 | 메모리 | 파일 (Documents, UserDefaults 등) |
| 활용 사례 | 화면 간 상태 공유 | 영구적인 설정, 사용자 데이터 저장 |
위에서 사용한 InMemoryKey는 앱이 실행되는 동안에만 상태를 유지하므로, 앱을 종료하면 Stats 데이터가 초기화됩니다. 반면, FileStorageKey를 사용하면 Stats 데이터를 파일로 저장하여 앱이 종료된 후에도 값을 유지할 수 있습니다.
- @Shared를 사용하면 여러 Reducer에서 상태를 공유할 수 있다.
- InMemoryKey는 앱이 실행되는 동안에만 데이터를 유지한다.
- FileStorageKey를 활용하면 앱이 종료되어도 데이터를 유지할 수 있다.
- @Shared(.stats)를 사용하여 CounterTab과 ProfileTab에서 동일한 데이터를 사용할 수 있다.
이제 여러분도 TCA에서 Shared 상태를 활용하여 강력한 상태 관리를 구현해보세요! 🚀🔥
다음 글에서 더 흥미로운 TCA 활용 사례를 다뤄보겠습니다. 감사합니다! 😊