Store 패턴을 만들게 된 이유를 다시 한 번 상기시키고 넘어가고자 한다.
1. Single Source 가 지켜지지 않아, 각 ViewModel가 동일한 데이터를 여러 상태로 중복으로 관리하고 있는 문제가 존재함. 이는 하나의 상태만 바뀐다거나 어느 값이 원천 값인지 구분하기 어렵게 만든다.
2. SwiftUI 에서 양방향 데이터 바인딩으로 인해 값이 변화되는 것을 추적하기 어렵다.
때문에 현재 구조를 무너뜨리지 않는 선에서 아키텍처를 새롭게 만들고자 하였다.

Store 패턴

현재 Store 패턴의 도식화이다. 보면 Flux과 TCA의 모습과 비슷하다.
하지만 Flux 패턴은 Store가 여러 개이기 때문에 Store들이 함께 사용되어야 하는 경우 의존성을 관리하는 것에 복잡함이 있다고 느꼈다.
또한 TCA는 Middleware 없이 Reducer에서 SideEffect 역할을 함께 수행한다. Pointree 에 TCA 개발자가 나와서 이야기 하는 것을 들어보면 Middleware를 사용함으로써 Reducer의 흐름을 가로채거나 Reducer 외부에서 Side Effect를 처리해, 단일한 데이터 흐름이 깨지는 것을 피하고자 한다고 했다. 하지만 나는 Middleware로 역할을 분리하는 것이 더 예측가능하다고 생각해 Middleware를 만들게 되었다.
Store 패턴 흐름 파악하기
ViewModel -> Action
func getAllProfile() {
appStore.send(.profile(.getAllProfileData))
} // ViewModel
기존에 SwiftUI와 MVVM 패턴을 적용하고 있었다. Store 패턴을 도입하며 View와 연결된 ViewModel 까지 다시 처음부터 만들고싶지는 않았다. 그래서 ViewModel에서부터 시작된다. 즉, View와 ViewModel 사이의 수정은 필요하지 않았다.
Middleware -> Reducer
case .getAllProfileData:
Task {
do {
async let pet = try await getPet().async()
async let family = try await getFamily().async()
async let preSchool = try await getPreSchool().async()
async let profile = try await getBabyProfile().async()
let (petEntity, familyEntity, preSchoolEntity, profileEntity) = try await (pet, family, preSchool, profile)
send(.profile(.getPetEntityComplete(petEntity: petEntity)))
send(.profile(.getFamilyEntityComplete(familyEntity: familyEntity)))
send(.profile(.getPreSchoolEntityComplete(preSchoolEntity: preSchoolEntity)))
send(.profile(.getBabyProfileComplete(babyProfile: profileEntity)))
} catch {
send(.error(.setNetworkError))
Logger.error("\(error)")
}
} // Middleware
Middleware에서는 Action을 받아 해당 케이스를 구현한다. 네트워크 통신과 같은 작업을 주로 진행하도록 만들었다. 그리고 작업이 끝났을 때 "~~Complete"와 같이 네이밍을 구분해 수행하도록 만들었다.
Reducer -> State
public struct ProfileState: Equatable {
public var pet: PetEntity?
public var family: BabyFamilyEntity?
public var school: PreSchoolEntity?
}
public enum ProfileAction {
case saveAllProfileData
case saveAllProfileDataComplete
}
...
case .getPetEntityComplete(petEntity: let petEntity):
state.profile.petEntity = petEntity
// Reducer
...
Reducer는 Action을 받아 상태를 바꾸는 역할을 수행한다.
State -> ViewModel
appStore.profileStatePublisher
.receive(on: RunLoop.main)
.sink { [weak self] state in
guard let self = self else { return }
babyProfile = state.babyProfile
...
}
.store(in: &bag) // ViewModel
마지막으로 State가 바뀌면 바인딩을 통해 ViewModel에 있는 값을 변경시켜준다.
Store 패턴을 적용한 후 느낀 장점
1. 변경을 최소화
ViewModel을 그대로 사용함으로써 View에 대한 변경이 거의 필요하지 않았다.
또한 각 ViewModel에 상태값들은 결국 Store에서 바인딩되어 연결되있음을 확인할 수 있기 때문에 기존 값들의 활용도를 유지할 수 있었다.
2. 단방향 아키텍처
기존에는 ViewModel의 메서드로 값을 바꾸고 View에서는 .onChange 메서드로 값이 바뀜을 보고 있다가 값이 바뀌면 다른 액션을 또 다시 수행하고 하나의 문제를 마주했을 떄 이를 해결하기 위한 과정이 복잡했다.
action으로 보내고 State로 값을 바꿔주는 과정으로 고정되어 있기 때문에 추적이 수월해졌다.
하지만 아쉬움도 존재했다. ViewModel의 상태 값을 action이 아닌 여전히 onchange 등으로 바꿀 수 있다는 것이다. 이는 개발을 하면서 개발자끼리 action으로만 값을 변경하겠다고 약속을 했지만 잘 지켜질지는 두고봐야 할 것으로 보인다.
'개발경험' 카테고리의 다른 글
| 디자인 시스템 개발 이야기(3) - 함수형 체이닝을 통한 디자인 시스템 (0) | 2025.10.04 |
|---|---|
| 디자인 시스템 개발 이야기(2) - 기존 디자인 시스템(Constructor Hell) (0) | 2025.08.31 |
| 디자인 시스템 개발 이야기(1) - 아토믹 디자인 (0) | 2025.08.24 |
| [Swift] CMPedomter 러닝앱 케이던스 만들기 (3) | 2025.06.06 |
| [Swift Unit Test] "오운완" 효과적인 테스트를 위한 고민(코드 재사용) (0) | 2023.09.19 |