화면 상태 하나 바꾸려고 if문을 열 개씩 고쳐본 적 있으신가요?
로딩·성공·실패 상태를 전부 if isLoading, else if hasError 로 처리하다 보면 코드가 금세 감당이 안 되죠.
오늘은 Swift 스테이트 패턴으로 if문 지옥을 상태 객체로 정리하는 방법을 실제 경험 위주로 풀어볼게요.
먼저 핵심부터 말씀드리면, 스테이트 패턴은 각 상태를 별도의 객체(또는 enum)로 분리하고 상태 전환 로직을 그 객체 안에 넣는 것입니다. 그러면 조건 분기가 흩어지지 않고 한곳에 모여요.
이 글에서 얻어갈 것
- if문 지옥이 왜 생기는지
- Swift에서 상태를 객체로 나누는 두 가지 방법
- enum과 protocol 방식 중 언제 뭘 쓸지
- 실무에 바로 쓰는 리팩터링 순서
if문 지옥은 왜 생길까요?
처음엔 상태가 두어 개뿐이라 if로 충분해 보여요. 그런데 요구사항이 늘면서 상태가 5개, 6개로 불어납니다. 로딩, 성공, 실패, 빈 화면, 재시도 중… 이런 식으로요.
문제는 이 상태 조합이 여러 메서드에 흩어진다는 점이에요. 버튼 활성화 로직에도 if, 화면 갱신에도 if, 네트워크 콜백에도 if가 반복됩니다. 그래서 상태 하나를 추가하면 그 if 뭉치를 전부 찾아다니며 고쳐야 하고, 하나라도 빠뜨리면 곧장 버그가 돼요.
상태가 흩어지면 버그가 숨고, 상태가 모이면 버그가 드러납니다.
스테이트 패턴은 바로 이 “흩어짐”을 막아주는 구조예요.
Swift에서 상태를 객체로 나누는 법 (enum 방식)
가장 가볍게 시작하는 방법은 enum입니다. 상태를 값으로 정의하고 연관값으로 필요한 데이터까지 담을 수 있어요. 아래는 화면 상태를 enum으로 표현한 예시입니다.
enum ViewState {
case loading
case loaded(items: [String]) // 성공 시 데이터 동봉
case failed(message: String) // 실패 사유 동봉
case empty
}
이제 화면 갱신은 switch 한 번으로 끝납니다. 연관값 덕분에 “성공인데 데이터가 nil인” 애매한 상태 자체가 생기지 않아요.
switch state {
case .loading: showSpinner()
case .loaded(let items): render(items)
case .failed(let msg): showError(msg)
case .empty: showEmptyView()
}
저는 대부분의 화면 상태는 이 enum 방식으로 충분하더라고요. switch는 케이스를 빠뜨리면 컴파일러가 잡아주니 상태를 추가할 때도 마음이 편해요.
enum vs protocol, 언제 뭘 쓸까요?
상태마다 “행동”이 크게 다르고 전환 규칙이 복잡하면 protocol 방식이 낫습니다. 각 상태를 타입으로 만들고, 다음 상태를 스스로 반환하게 하는 구조예요. 두 방식의 차이를 정리하면 이렇습니다.
| 구분 | enum 방식 | protocol 방식 |
|---|---|---|
| 적합한 상황 | 상태가 데이터 중심 | 상태마다 행동이 다름 |
| 상태 추가 | case 추가 | 타입 추가 |
| 전환 로직 | 외부에서 switch | 상태 객체 내부에 캡슐화 |
| 러닝커브 | 낮음 | 다소 높음 |
protocol 방식은 상태 전환을 각 객체가 책임진다는 게 핵심이에요.
protocol PlayerState {
func play() -> PlayerState // 다음 상태를 반환
func pause() -> PlayerState
}
struct PlayingState: PlayerState {
func play() -> PlayerState { self } // 이미 재생 중이면 유지
func pause() -> PlayerState { PausedState() } // 일시정지로 전환
}
이렇게 하면 “재생 중일 때 pause를 누르면?” 같은 규칙이 그 상태 안에만 존재해요. 분기문이 사라지고, 각 상태가 자기 규칙만 알면 됩니다.
실무 리팩터링, 이 순서로 해보세요
한 번에 다 바꾸려 하면 오히려 겁이 나요. 저는 아래 순서로 조금씩 옮겼습니다.
- 흩어진
if가 실제로 표현하는 상태 목록을 종이에 적기 - 그 상태들을 enum 하나로 정의하기
- 화면 갱신 코드부터
switch로 교체하기 - 불가능한 상태 조합(예: 로딩+에러 동시)을 enum으로 아예 제거하기
- 전환 규칙이 복잡한 부분만 protocol로 승격하기
포인트는 enum으로 시작해서 필요할 때만 protocol로 올리는 것입니다. 처음부터 protocol로 과하게 설계하면 오히려 코드가 무거워지거든요.
자주 묻는 질문 (Q&A)
Q. 상태가 3개뿐인데도 패턴을 써야 하나요?
꼭 그럴 필요는 없어요. 다만 상태가 여러 메서드에 흩어져 있다면 개수와 무관하게 enum으로 모으는 게 좋습니다.
Q. SwiftUI에서도 쓸 수 있나요?
네. @Published var state: ViewState 형태로 두고 뷰에서 switch로 분기하면 아주 잘 맞아요.
Q. enum 연관값이 많아지면 지저분하지 않나요?
연관값이 3개를 넘어가면 별도 struct로 묶어서 case loaded(Result) 처럼 담는 걸 추천드려요.
처음엔 낯설어도 상태를 객체로 모으는 감을 한 번 잡으면 이전 코드로 돌아가기 싫어질 거예요. 작은 화면 하나부터 enum으로 정리해보세요. if문 지옥에서 생각보다 빨리 벗어나실 겁니다. 응원할게요!
