반응형
SwiftUI Study – 상태 변경을 한 곳으로 모으는 패턴
1. 왜 중요한가 (문제 배경)
SwiftUI는 “상태가 변하면 View를 다시 그린다”는 매우 단순한 규칙으로 움직입니다.
그런데 하나의 상태를 여러 계층에서 동시에 변경하기 시작하면 다음과 같은 문제가 생깁니다.
- 버튼 한 번 눌렀을 뿐인데 상태가 두 번 토글됨
- 시트가 잠깐 떴다가 바로 사라지는 깜빡임
- 네비게이션이 두 번 push 되거나 pop 순서가 꼬임
- 동일 API가 중복 호출됨
문제의 본질은 간단합니다.
같은 상태를 여러 곳에서 제각각 바꾸고 있다.
그래서 실무에서는 “이 상태를 실제로 바꾸는 곳은 단 하나” 라는 원칙을 세우는 것이 중요합니다.
2. 잘못된 패턴 예시
❌ 예시 1: Button과 onChange에서 같은 상태를 모두 변경하는 경우
struct WrongView: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("스위치", isOn: $isOn)
Button("토글 버튼") {
isOn.toggle() // ① 버튼에서 상태 변경
}
}
.onChange(of: isOn) { newValue in
if newValue {
isOn.toggle() // ② onChange에서 다시 상태 변경
}
}
}
}
문제점
- isOn이 true로 바뀌는 순간 onChange가 다시 토글을 호출
- 최종 상태는 다시 false가 되어, 사용자는 “버튼이 안 먹는다”고 느끼게 됨
- 어디에서 상태가 바뀌는지 추적이 어려워 디버깅 난이도가 높아짐
❌ 예시 2: View와 ViewModel이 동시에 같은 상태를 건드리는 경우
final class CounterViewModel: ObservableObject {
@Published var count = 0
func increase() {
count += 1
}
}
struct WrongCounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
Button("증가: \(viewModel.count)") {
viewModel.increase() // ① ViewModel에서 증가
viewModel.count += 1 // ② View에서 또 증가
}
}
}
문제점
- 버튼 한 번에 count가 2씩 증가
- 나중에 로직을 수정하면 어디를 고쳐야 할지 모호해짐
- View와 ViewModel 간 역할 경계가 무너짐
3. 올바른 패턴 예시
✅ 예시 1: 상태 변경 진입점을 하나로 모으기
struct SingleSourceView: View {
@State private var isOn = false
var body: some View {
VStack {
Toggle("스위치", isOn: $isOn)
Button("토글 버튼") {
toggle() // 상태 변경은 이 함수 한 곳에서만
}
}
}
private func toggle() {
isOn.toggle()
// 부수 효과가 있다면 모두 여기에서 처리
}
}
특징
- isOn을 변경하는 코드는 toggle() 안에만 존재
- 나중에 로직이 복잡해져도 이 함수 하나만 보면 됨
✅ 예시 2: 상태 변경은 ViewModel 메서드로만, View에서는 메서드만 호출
final class CounterViewModel: ObservableObject {
@Published private(set) var count = 0
func increase() {
count += 1
// 로깅, 분석, 추가 로직 등도 여기서 함께 수행
}
}
struct CorrectCounterView: View {
@StateObject private var viewModel = CounterViewModel()
var body: some View {
Button("증가: \(viewModel.count)") {
viewModel.increase() // View는 오직 메서드 호출만
}
}
}
특징
- count 변경은 ViewModel 내부에서만 발생
- View는 “어떤 UI에서 어떤 액션을 발생시켰는지”만 표현
- 테스트할 때도 ViewModel만 떼어 검증 가능
✅ 예시 3: onChange에서는 상태를 “관찰”만 하고, 다시 변경하지 않기
struct OnChangeView: View {
@State private var text = ""
var body: some View {
TextField("검색어 입력", text: $text)
.onChange(of: text) { newValue in
// ✅ 상태 변경 대신, 부수 효과만 수행
print("검색어 변경: \(newValue)")
// 예: 디바운스 후 검색 API 호출 등의 트리거
}
}
}
원칙
- onChange에서는 이미 변경된 상태를 보고 부수 효과를 수행
- 그 상태를 다시 변경하지 않는다
4. 실전 적용 팁
✔ 팁 1 – “이 상태를 실제로 바꾸는 함수는 어디인가?”를 항상 의식하기
- 하나의 상태마다 “실제 쓰기(write)”가 발생하는 진입점이 몇 개인지 점검
- 2곳 이상이면 구조를 재설계할 신호로 보아야 함
✔ 팁 2 – View는 가능하면 상태를 직접 건드리지 말고, 메서드를 호출하는 쪽으로
- 특히 비즈니스 로직이 함께 얽힌 상태는 ViewModel/Reducer에 모으는 것이 좋음
✔ 팁 3 – onChange / onAppear / task 내부에서 다시 동일 상태를 바꾸지 말기
- 대부분의 “깜빡임, 두 번 실행” 문제는 여기서 발생
- 이곳에서는 부수 효과(side effect)만 실행하는 것을 원칙으로 삼기
✔ 팁 4 – TCA나 Redux 스타일 아키텍처의 핵심도 결국 “단일 상태 변경 경로”
- Action → Reducer → State 변경 흐름처럼
모든 변경이 한 함수(또는 한 레이어)를 거치게 만드는 것이 가장 안전한 구조
5. 정리
- 하나의 상태를 여러 계층에서 동시에 변경하면 UI 버그, 중복 실행, 디버깅 지옥으로 이어진다.
- 상태 변경은 “단일 진입점(single source of mutation)”을 갖도록 설계하는 것이 중요하다.
- View는 가급적 상태 변경 메서드를 호출만 하고, 실제 변경은 ViewModel/Reducer에서 처리한다.
- onChange, onAppear 등에서는 변경된 상태를 관찰만 하고 다시 변경하지 않는다.
이 원칙을 프로젝트 전반에 적용해두면,
SwiftUI 특유의 “왜 두 번 실행되지?” “왜 깜빡이지?” 같은 문제를 크게 줄일 수 있습니다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – AnyView 남용을 피하고 타입 안정성을 유지하는 방법 (0) | 2025.12.09 |
|---|---|
| SwiftUI Study – View 중첩이 깊어지지 않도록 컴포넌트를 분리하는 실전 패턴 (0) | 2025.12.09 |
| SwiftUI Study – offset / position으로 레이아웃을 잡지 말아야 하는 이유 (0) | 2025.12.09 |
| SwiftUI Study – GeometryReader에 의존하지 않고 레이아웃을 안정적으로 구성하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – FocusState를 안전하게 설계하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – 도메인 상태와 UI 상태를 분리하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – @State / @Binding / @ObservedObject / @StateObject / @EnvironmentObject 정확한 역할 이해하기 (0) | 2025.12.09 |
| SwiftUI에서 많이 하는 실수 - Button 안에서 상태 변경 로직이 여러 계층에 중첩되어 UI가 비정상 업데이트되는 문제 (0) | 2025.12.08 |

