SwiftUI에서 많이 하는 실수 - Button 안에서 상태 변경 로직이 여러 계층에 중첩되어 UI가 비정상 업데이트되는 문제
Dev Study/SwiftUI 2025. 12. 8. 19:53반응형
SwiftUI에서 많이 하는 실수 - Button 안에서 상태 변경 로직이 여러 계층에 중첩되어 UI가 비정상 업데이트되는 문제
1. 문제 원인
SwiftUI에서는 하나의 상태(State)를 “어디에서” 변경하는지가 매우 중요합니다.
그런데 실무 코드에서는 다음과 같은 패턴이 자주 섞여 있습니다.
- Button action 내부에서 @State / @Binding / ObservableObject 값을 변경
- 같은 상태를 부모 뷰의 .onChange(of:) 나 .task, .onAppear 등에서 추가로 변경
- ViewModel 내부에서도 같은 상태를 다시 변경
즉, 한 번의 버튼 탭이 여러 계층에서 같은 상태를 중첩해서 변경하는 구조가 되면서,
- 상태가 두 번 토글되거나
- 네비게이션/시트가 열렸다 닫혔다 깜빡이거나
- 예상과 다른 최종 상태로 귀결되는
비정상적인 UI 업데이트가 발생합니다.
문제의 핵심은:
“같은 상태를 여러 레벨에서 동시에 관리하고,
버튼 탭을 기준으로 어디서 무엇을 변경하는지 경계가 모호한 설계”
입니다.
2. 나타나는 증상
아래와 같은 현상이 나타날 수 있습니다.
- 버튼을 눌렀는데 토글 값이 바뀌지 않은 것처럼 보임 (두 번 토글되어 원래 값으로 돌아감)
- .sheet(isPresented:), .fullScreenCover 등이 잠깐 떴다가 바로 사라지는 깜빡임
- NavigationStack push가 두 번 일어나거나, pop 상태가 꼬여 화면 전환이 이상해짐
- 동일 API 호출이 두 번 연속 발생
- 애니메이션이 의도와 다르게 뒤집히거나, 중간 상태 없이 순간이동하듯 바뀜
이런 문제는 대개 한 번의 탭이 여러 곳에서 상태 변경을 일으키고 있다는 신호입니다.
3. 잘못된 코드 예시
아래 예시는 부모 뷰와 자식 뷰가 같은 상태를 각각 따로 변경하는 전형적인 안 좋은 패턴입니다.
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "ON" : "OFF")
ToggleRow(isOn: $isOn)
}
.onChange(of: isOn) { newValue in
// 상태가 true가 되면 다시 false로 되돌리는 이상한 로직
if newValue {
isOn.toggle()
print("Parent onChange에서 다시 토글, isOn: \(isOn)")
}
}
}
}
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View {
Button {
isOn.toggle()
print("Button action에서 토글, isOn: \(isOn)")
} label: {
Text("토글 버튼")
}
}
}
어떤 일이 벌어지나?
초기 상태: isOn == false
- 버튼 탭 → Button action 실행 → isOn 이 false → true 로 변경
- 이 변경을 감지한 ParentView.onChange(of: isOn) 실행 → isOn.toggle() 다시 호출
- isOn 이 true → false 로 되돌아감
결과적으로:
- 버튼을 눌렀는데 최종 상태는 여전히 false
- 콘솔에는 “Button action에서 토글” + “Parent onChange에서 다시 토글” 두 번 로그
- 사용자는 “버튼이 안 먹는다”고 느끼게 됨
이와 유사하게, 시트/네비게이션 상태에 이런 구조가 들어가면
실제 단말기에서는 “팝업이 깜빡이는 것처럼 보이는” 현상이 발생할 수 있습니다.
4. 올바른 코드 예시
예시 1: 상태 변경 책임을 버튼 쪽에만 두기
부모의 .onChange 에서 같은 상태를 다시 변경하지 않고,
실제로 상태를 바꾸는 책임은 한 곳(여기서는 Button action)에만 둡니다.
struct ParentView: View {
@State private var isOn = false
var body: some View {
VStack {
Text(isOn ? "ON" : "OFF")
ToggleRow(isOn: $isOn)
}
.onChange(of: isOn) { newValue in
// 여기서는 로그, 추적, 부수 효과만 수행
print("상태 변경 감지: \(newValue)")
// isOn.toggle() 같은 '다시 상태 변경'은 하지 않음
}
}
}
struct ToggleRow: View {
@Binding var isOn: Bool
var body: some View {
Button {
isOn.toggle()
print("Button action에서 한 번만 토글, isOn: \(isOn)")
} label: {
Text("토글 버튼")
}
}
}
이제:
- 버튼을 누르면 isOn 은 false → true, true → false 로 정상 토글
- .onChange 는 변경을 “관찰”만 하고, 다시 상태를 건드리지 않음
- 최종 상태는 사용자가 기대한 대로 유지된다.
예시 2: ViewModel로 상태 변경을 위임하고 뷰에서는 호출만
상태를 여러 레벨에서 건드리지 않고,
ViewModel의 메서드 하나만이 상태 변경을 담당하게 만들면 훨씬 안전합니다.
final class ToggleViewModel: ObservableObject {
@Published var isOn = false
func toggle() {
isOn.toggle()
print("ViewModel에서 상태 변경, isOn: \(isOn)")
}
}
struct ParentWithViewModel: View {
@StateObject private var viewModel = ToggleViewModel()
var body: some View {
VStack {
Text(viewModel.isOn ? "ON" : "OFF")
Button {
viewModel.toggle()
} label: {
Text("토글 버튼")
}
}
// onChange에서는 부수 효과만
.onChange(of: viewModel.isOn) { newValue in
print("상태 변경 감지: \(newValue)")
}
}
}
이 구조에서는:
- “상태는 ViewModel이 관리”
- “View는 toggle()을 호출만”
- “부수 효과는 onChange에서 실행하되, 다시 상태를 바꾸지 않음”
이라는 역할 분리가 이루어져,
한 탭이 여러 계층에서 중첩 상태 변경을 일으키는 설계를 피할 수 있습니다.
5. 정리 및 팁
- 하나의 상태(@State, @Binding, @Published)를 여러 레벨에서 동시에 변경하는 설계는 버그의 근원입니다.
- 특히 Button action + .onChange(of:) + ViewModel 내부 로직에서
같은 값을 모두 건드리는 구조는, 토글/네비게이션/시트 상태를 꼬이게 만듭니다. - 원칙적으로는 다음을 지키는 것이 안전합니다.
- “이 상태를 실제로 변경하는 책임자는 한 곳이다.”
- 나머지 곳에서는 변경을 관찰만 하거나, 부수 효과(side effect)만 수행한다.
- SwiftUI에서는 “상태의 단일 소유자(single source of truth)” 원칙이 특히 중요합니다.
이 원칙이 깨지는 순간, 한 번의 버튼 탭이 여러 계층에서 중첩 업데이트를 발생시키며
사용자에게는 “버튼이 안 먹는 것 같은” 이상한 UI로 보이게 됩니다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – 상태 변경을 한 곳으로 모으는 패턴 (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에서 많이 하는 실수 - 하나의 상태를 여러 곳에서 동시에 수정해 충돌이 나는 실수 (1) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - AppStorage / SceneStorage를 남용해 예기치 않은 상태 유지가 발생하는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - View 중첩이 지나치게 깊어져 가독성과 유지보수가 나빠지는 실수 (0) | 2025.12.05 |
| SwiftUI에서 많이 하는 실수 - Modifier 순서를 이해하지 못해 스타일이 적용되지 않는 실수 (0) | 2025.12.05 |

