
SwiftUI 방식으로 기록 통계 화면을 만들면서 고민되었던 property 간의 동기화 방법에 대해 gemini-2.5-pro 와 논의했던 내용을 토대로 gemini가 정리해준 내용을 남기고자 한다
네, 그럼요. 저희가 나눈 이야기를 바탕으로 멋진 블로그 글을 작성하실 수 있도록 핵심 내용을 마크다운 형식으로 정리해 드릴게요.
SwiftUI와 Combine을 사용해 앱을 만들다 보면 여러 ViewModel 간에 상태를 동기화해야 하는 상황을 마주하게 됩니다. 특히 부모-자식 관계를 갖는 View와 ViewModel 구조에서 자식의 상태 변화를 다른 자식에게 전달해야 할 때, 어떻게 설계하는 것이 가장 효율적이고 확장성이 좋을까요?
selectedDate를 어떻게 공유할까?세 개의 ViewModel이 다음과 같은 부모-자식 관계를 맺고 있습니다.
LogViewModel (부모)CalendarViewModel (자식)LogSummaryViewModel (자식)CalendarView에서 날짜를 선택하면(CalendarViewModel의 selectedDate 변경) 이 변경 사항이 LogSummaryView에도 반영되어야 합니다. 즉, 여러 ViewModel이 selectedDate라는 하나의 상태를 공유해야 합니다.
// LogViewModel (Parent)
final class LogViewModel: ObservableObject {
// 부모가 직접 selectedDate를 소유하려 하지만,
@Published private(set) var selectedDate: Date = Date()
// 자식 ViewModel들을 프로퍼티로 소유함
var calendarViewModel = CalendarViewModel()
var summaryViewModel = LogSummaryViewModel()
private var cancellables = Set<AnyCancellable>()
init() {
// 자식(Calendar)의 변화를 구독하여 자신의 상태를 업데이트
calendarViewModel.$selectedDate
.sink { [weak self] date in
self?.selectedDate = date
// TODO: 이 시점에 summaryViewModel에도 알려줘야 하나?
}
.store(in: &cancellables)
}
}
// CalendarViewModel (Child)
final class CalendarViewModel: ObservableObject {
// 자식이 직접 selectedDate를 @Published로 소유
@Published private(set) var selectedDate: Date
// ...
func send(_ action: Action) {
switch action {
case .selectDate(let day):
// CalendarView의 액션에 따라 자신의 상태를 직접 변경
selectedDate = day
// ...
}
}
}
// LogSummaryViewModel (Child)
final class LogSummaryViewModel: ObservableObject {
// 이 ViewModel도 selectedDate가 필요하지만, 어떻게 받아야 할까?
@Published private(set) var selectedDate: Date = Date()
// ...
}
처음에는 Combine의 sink를 이용해 데이터 흐름을 연쇄적인 파이프라인처럼 구성했습니다.
흐름:
CalendarView에서 날짜 선택 → ① CalendarViewModel.selectedDate 변경 → ② LogViewModel이 sink로 변경 감지 후 LogViewModel.selectedDate 업데이트 → ③ LogViewModel이 LogSummaryViewModel의 update 함수 호출
이 방식은 "누가, 누구에게, 무엇을 전달하는지" 흐름이 명확하게 보인다는 장점이 있습니다.
// LogViewModel.swift
init() {
calendarViewModel.$selectedDate
.sink { [weak self] newDate in
// 1. 자식의 변화를 감지하여 자신의 상태를 동기화
self?.selectedDate = newDate
// 2. 다른 자식에게 변경사항을 직접 전달 (함수 호출 방식)
self?.summaryViewModel.send(.selectDate(newDate))
}
.store(in: &cancellables)
}
// LogSummaryViewModel.swift
final class LogSummaryViewModel: ObservableObject {
// ...
enum Action {
case selectDate(Date)
}
func send(_ action: Action) {
switch action {
case .selectDate(let date):
self.selectedDate = date
// ... 날짜 변경에 따른 UI 업데이트 로직
}
}
// ...
}
파이프라인 방식은 단순한 흐름에선 명확하지만, 앱이 복잡해지면 문제가 생길 수 있습니다. 예를 들어, LogSummaryView에 "오늘로 이동" 버튼이 생긴다면 어떻게 될까요? SummaryVM → LogVM → CalendarVM 으로 이어지는 역방향/순방향 데이터 흐름이 거미줄처럼 얽히게 됩니다.
이것이 "단일 진실 공급원 (Single Source of Truth, SSoT)" 패턴이 더 나은 개선안이 되는 이유입니다.
핵심 아이디어: 공유되어야 할 상태(selectedDate)는 오직 하나의 객체(부모, LogViewModel)만이 소유하고 관리합니다. 다른 자식 View들은 이 원천 데이터를 빌려 쓰거나(@Binding), 변경을 구독할 뿐입니다.
장점:
LogViewModel)에서 일어나고 모든 구독자에게 자동으로 전파됩니다. 새로운 기능이 추가되어도 허브에 연결만 하면 되므로 구조가 단순하게 유지됩니다.허브 ↔ 자식 형태로 단순화되어 사이드 이펙트를 예측하기 쉽습니다.// LogViewModel (The Hub - Source of Truth)
final class LogViewModel: ObservableObject {
// 1. 부모가 유일하게 상태를 소유하고 외부에 공개 (@Published)
@Published var selectedDate: Date = Date()
// ...
}
// LogView (Parent View)
struct LogView: View {
@StateObject private var viewModel = LogViewModel()
var body: some View {
VStack {
// 2. 자식 View에게 상태를 Binding으로 전달
// 이제 CalendarView는 selectedDate를 직접 수정하는 '권한'이 아닌
// '변경을 요청하는 통로'를 갖게 됨
CalendarView(selectedDate: $viewModel.selectedDate)
// 3. 다른 자식 View에게는 읽기 전용으로 전달
LogSummaryView(selectedDate: viewModel.selectedDate)
}
}
}
// CalendarView (Child View)
struct CalendarView: View {
// 4. @Binding으로 부모의 상태와 양방향 연결
@Binding var selectedDate: Date
var body: some View {
// DatePicker 등 selectedDate를 직접 변경하는 UI 요소들
DatePicker("Select Date", selection: $selectedDate, displayedComponents: .date)
.labelsHidden()
}
}
// LogSummaryView (Child View)
struct LogSummaryView: View {
// 5. 부모로부터 받은 읽기 전용 상태
let selectedDate: Date
var body: some View {
Text("Selected: \(selectedDate, formatter: DateFormatter())")
// selectedDate가 바뀌면 이 View는 자동으로 업데이트됨
}
}
위 코드에서 ViewModel을 View에 직접 주입하는 대신, selectedDate 값 자체를 주입하여 View가 더 독립적이고 재사용 가능하도록 설계했습니다.
| 구분 | 파이프라인 방식 | Single Source of Truth 방식 |
|---|---|---|
| 핵심 개념 | 역할에 따른 데이터 흐름의 방향성 | 상태의 중앙 집중 관리 |
| 데이터 흐름 | 자식1 → 부모 → 자식2 (연쇄적) | 자식 ↔ 부모 (허브-스포크) |
| 장점 | 단순한 구조에서 흐름이 명확함 | 확장성, 유연성, 유지보수 용이성 |
| 단점 | 복잡한 요구사항 발생 시 스파게티 코드화 | @Binding의 동작에 대한 초기 이해 필요 |
결론적으로, 여러 View와 ViewModel이 동일한 상태를 공유하고 수정해야 한다면 Single Source of Truth 패턴을 적용하는 것이 장기적으로 훨씬 유리합니다.
앞으로 앱을 설계할 때 다음과 같은 질문을 던져보세요.
"이 상태 데이터는 궁극적으로 누가 소유해야 하는가?"
이 질문에 대한 답을 찾고 그 객체를 "진실의 원천"으로 삼으세요. 그리고 다른 모든 컴포넌트는 그 원천을 참조하거나, 변경을 요청하는 방식으로 설계한다면 훨씬 더 견고하고 확장 가능한 SwiftUI 앱을 만들 수 있을 것입니다.
추가로 질문했었던 프롬프트, 그리고 블로그 글 정리 요청 프롬프트도 남깁니다~!
@Published selectedDate 값 변화에 대해 자식 -> 부모, 혹은 부모 -> 자식으로 전달하는 방법에 대해 내가 맞게 접근했는지, 올바른 방법이 있는지를 확인해줘.
먼저 세 가지 ViewModel 및 View 간의 관계는 다음과 같아.
- LogViewModel 이 CalendarViewModel, LogSummaryViewModel 를 자식으로 지니고 있음
- ViewModel 간의 관계와 마찬가지로 LogView 화면 내에서 CalendarView, LogSummaryView 를 표시하고 있음
확인하고 싶은 상황은 다음과 같아.
- CalendarView 내에서 날짜를 선택하면 -> CalendarViewModel 의 @Published var selectedDate 값이 변화됌
- LogViewModel 내에서는 이 calendarViewModel 의 $selectedDate 값을 sink 하여 변화가 인지되면 -> LogViewModel 의 @Published var selectedDate 값을 변화시킴
이렇게 동작은 되고 있는데 올바르게 접근한게 맞을까?
그리고 아래 상황을 추가하고 싶다면 어떻게 접근하는게 좋을까?
- LogSummaryView 에서도 selectedDate 값 변화에 따른 UI 변화가 필요함
- LogViewModel 내에서는 이 calendarViewModel 의 $selectedDate 값을 sink 하는 시점에 logSummaryViewModel.updateSelectedDate(to: Date) 와 같은 함수를 호출해서 전달해야 하는지
-> 현재 접근 방식에 대한 검토, 그리고 Single Source of Truth 패턴에 대한 제안 등을 알려주면서 아래 결론 내용을 답변했습니다.

추가 질문이야.
장기적인 개선: 앱이 더 복잡해진다면, LogViewModel이 selectedDate를 직접 @Published로 소유하고, 자식 View들에게는 Binding 이나 Combine 구독을 통해 상태를 전달하는 "단일 진실 공급원" 패턴으로 리팩토링하는 것을 고려해보세요.
이 방식을 사용하게 되면
장점으로는 -> LogViewModel 내에만 단일 프로퍼티로 존재하기에 여러 프로퍼티간의 "동기화"시킬 과정 없이 "single source of truth" 의미가 강해짐
내가 느낀 단점은 -> 자식, 부모 모두 해당 값을 수정하고 변화를 수신받을 수 있게 되므로 -> 어느 시점에 어디서 값을 변경시키는지, 값이 변경되었을 때 어떻게 동작되는지가 조금 모호해진 형태로 느껴질수도 있을 것 같다고 생각돼
만약 지금 구조를 유지한다면 -> 부모는 자식의 변화를 assign 한다는 방향이 생기고, 부모는 자식에게 send 한다 라는 방향이 생기게 되므로 좀 더 흐름이 읽히지 않을까? 라는 생각이 들어서말이야
single source of truth 에 대해 이런 프로퍼티의 수정, 변화 수신 관점에 대해 더 좋은 방향이 맞는지를 구체적으로 좀 더 설명해줄 수 있을까?
-> 장단점 모두 매우 정확하게 파악한다는 입바른 소리를 시작으로 현재 구조의 흐름 VS SSoT의 흐름을 비교해줬고
-> "모호함"에 대한 우려에 대해 Binding 동작에 대한 구체적인 설명을 말해줬고
-> SSoT 패턴이 확장성에서 뛰어난 이유에 대해 가정 상황을 토대로 설명해줬고
-> 결론 내용을 답변했습니다.


와우... 똑똑하구나 잘 이해했고 잘 설명해줬고 정리까지 만족스러운데?
논의했던 내용을 토대로 블로그글을 작성해보고 싶어. 핵심 내용들만 추스려서 MarkDown 스타일로 만들어줄 수 있을까?
# 고민 상황
부모 자식간의 관계가 있는 ViewModel, View 간의 상황과
부모, 자식간의 @Published var selectedDate 프로퍼티 변화에 대한 공유가 필요한 상황
## 고민 상황에 대한 핵심 코드들
LogViewModel, CalendarViewModel, LogSummaryViewModel 코드를 참고해서 셋 간의 관계 및 프로퍼티들간의 관계에 대해 이해가 될 수 있는 핵심 코드 정리
# 접근 방법 및 이 방법에 대한 검토
내가 구현했었던 방법(단방향 파이프라인), 이 방법에 대한 분석
언제 어디서 -> 어디로 라는 방향이 잘 인지되도록 구체적으로 설명
## 접근 방법에 대한 핵심 코드들
LogViewModel, CalendarViewModel, LogSummaryViewModel 코드를 참고해서 내가 어떻게 구현했었는지 이해가 될 수 있는 핵심 코드 정리
# 개선안
단 파이프라인 방식을 썼을 때의 장점과 단점은 무엇인지
그리고 SSoT 방식은 왜 더 개선안이 될 수 있는지
## 개선안 핵심 코드들
SSoT 방식으로 개선했을 때 기준으로 핵심 구조가 이해될 수 있는 코드 정리
# 요약
그래서 핵심은 무엇이였고, 어떤 것을 앞으로 중점으로 설계해야 하는지, 앞으로 어떤 상황이 생기면 어떤 것을 고려해야하는지 등을 요약
-> 이 결과로 지금 글이 작성됐습니다!