UIKit / SwiftUI에서는 무슨 디자인 패턴을 사용해야할까?

2025. 10. 17. 10:32·iOS

UIKit으로 개발할 때, 저는 Clean Architecture + MVVM + Coordinator 조합을 사용했어요.

사실 처음엔 누구나 그렇듯 MVC 패턴으로 시작했죠.

근데 이게... ViewController가 점점 비대해지더라고요. 전형적인 Massive View Controller 문제가 발생 🚨🚨

첫 번째 시도: 상태를 Struct로 분리

struct UserState {
    let name: String
    let email: String
}

class ViewController: UIViewController {
    private var state: UserState
    
    func updateUI() {
        nameLabel.text = state.name
        emailLabel.text = state.email
    }
}

상태를 분리했는데... 뭐가 줄어든 건지 모르겠더라고요? ViewController의 책임은 그대로였어요.

 

두 번째 시도: Class로 변경하고 반응성 추가

class ViewController {
    private var form = IssueForm()
    
    func textFieldDidChange(_ text: String) {
        form.title = text
        updateUI() // 😱 매번 호출
    }
    
    private func updateUI() {
        // 중복 코드
        navigationItem.rightBarButtonItem?.isEnabled = form.isValid
        titleErrorLabel.isHidden = !form.title.isEmpty
    }
}


struct IssueForm {
    var title: String = ""
    
    // ❌ 불가능! Struct는 값 타입이라 복사될 때마다 새 인스턴스 생성
    var titlePublisher: AnyPublisher<String, Never> 
}

근데 문제가 있었어요. Struct는 값 타입이라 복사될 때마다 새 인스턴스가 생성되잖아요?
그래서 Combine의 Publisher를 사용할 수 없었어요

  • 그래서 Class로 변경해주었습니다.

  • Combine으로 반응성도 확보하고, Publisher 소유권도 명확하게 하고, 메인 스레드 안정성도 보장할 수 있었어요.
    근데... 여전히 ViewController가 비대했어요.☝🏼

세 번째 시도: Presentation 로직 분리

그래서 도메인 모델을 View 요소로 바꾸는 역할도 분리했어요.
기존에는 ViewController가 도메인 영역의 UseCase를 직접 호출했다면, 이제는 ViewModel이 그 책임을 가져가도록 했죠.

결과: 의도치 않게 MVVM 패턴이 완성되었습니다 🫨🫨

 

네 번째 시도: 단방향 데이터 플로우

MVVM으로 책임은 분리되었지만, 새로운 문제가 생겼어요. 양방향 바인딩의 문제점이요.

어디서 상태를 수정하는지 추적이 어려웠어요.
View에서도 직접 접근이 가능하고, ViewModel에서도 메서드를 통해 변경이 가능하니까요. 

최종 해결책: Input/Output 패턴

  • Input 프로토콜: 뷰에서 발생하는 모든 사용자 액션들을 메서드로 정의합니다.
  • Output 프로토콜: 뷰모델의 상태 변화를 AnyPublisher를 통해 외부에 노출합니다.
  • 단방향 데이터 흐름 확립: UI Event → Input → Internal Logic → Output → UI Update라는 예측 가능한 흐름 만듬.


SwiftUI에서는?

SwiftUI는 설계 철학부터 단방향 데이터 흐름이라는 거예요.. WWDC19 "Data Flow Through SwiftUI"에서도 

모든 데이터는 SSOT(Single Source of Truth)를 지켜야 하고,
상위 뷰에서 하위 뷰로 데이터가 전달되는 단방향 흐름 구조여야 한다.

 

공식 문서나 튜토리얼 어디를 봐도 같은 메시지가 반복됩니다:

  • "데이터의 불일치를 방지하고, 일관된 UI 업데이트를 보장하기 위해 반드시 단방향 흐름 및 단일 원천에 따라야 한다"
  • "SwiftUI는 선언적 UI 프레임워크로, 상태가 변하면 자동으로 뷰를 업데이트한다"

SwiftUI의 핵심 동작 원리

  1. 사용자가 액션을 수행
  2. @State, @Binding 같은 상태 프로퍼티가 변경
  3. SwiftUI 런타임이 자동으로 감지
  4. Diffing 알고리즘으로 변경된 부분만 계산
  5. 해당 부분만 효율적으로 렌더링

UIKit은 개발자가 UI를 직접 제어하는 명령형 방식이라 자연스럽게 데이터 흐름이 양방향으로 복잡해졌어요.
하지만 SwiftUI는 UI를 데이터의 함수로 선언하는 
선언형 방식이기 때문에, 데이터는 항상 SSOT로 단방향 흐름을 선호하게 되는 거죠.

그러면 어떤 패턴을 사용할까요?

MV 패턴

MV 패턴은 이름 그대로 Model과 View로 이루어져 있습니다.

MV의 구성 요소

1. Model: 데이터와 비즈니스 로직

struct Post: Codable, Identifiable {
    let id: String
    let author: String
    let content: String
    var isLiked: Bool
    
    // 비즈니스 로직 포함
    var formattedLikeCount: String { /* ... */ }
}

2. View: 상태 관리와 렌더링

struct FeedView: View {
    ㅣet useCase = FetchUseCase()
    @State private var viewState: ViewState = .loading
    
    var body: some View {
        switch viewState {
            case .loading: ProgressView()
            case .loaded(let posts): PostList(posts: posts)
        }
      	.onAppear {
          useCase.execute()
        }
    }
}

뷰에서 직접 UseCase의 비즈니스 로직을 사용하고 필요한 SSOT는 모두 뷰에서 관리를 해주는 역할을 맡습니다.

그러면 텍스트필드같은 검증 로직이 필요하면 어디서 할까요?

struct ProfileView: View {
    @State private var state = ProfileState()
    
    var body: some View {
        Form {
            TextField("Name", text: $state.name)
            Button("Save") { if state.isValid() { save() } }
        }
    }
}

struct ProfileState {
    var name = ""
    var errors: [String] = []
    
    mutating func isValid() -> Bool {
        // 검증 로직
    }
}

이런식으로 별도의 Struct 자체에 검증 로직도 구현하면 되죠. 

그런데 뭔가 맨 위에서 UIKit 에서 상태만 분리한다...와 비슷해지지 않았나요?

네 맞아요 MV 패턴이 정말 상태만 Model로 만들고 View에 띄워주는 역할인거 같아요.

 

내가 생각한 장점

  • SwiftUI 철학에 가장 충실해요: 뷰는 데이터를 표현할 뿐, 모든 것은 데이터의 변화로 이루어진다는 철학에 완벽히 부합해요
  • 엄청 단순해서 러닝 커브가 거의 없어요: 개발 속도가 빨라지죠

하지만 어떤 단점이 있을까요?

  • 테스트 코드 작성이 어렵습니다.
     FeedView 예시처럼 뷰가 UseCase와 같은 의존성을 직접 포함하면, 단위 테스트를 위해 뷰를 테스트하기가 까다로워져요
  • 뷰가 비대해집니다. 
    복잡한 화면의 경우 @State, onAppear, onChange 등 수많은 로직이 뷰에 쌓이면서 뷰 자체가 거대해집니다.
    이는 UIKit의 Massive View Controller 문제와 유사해져버리는 거죠

자 그레서 여전히 MVVM을 사용하는 개발자들도 많아요:) UIKit을 꾸준히 해왔고 다른 패턴과도 비슷하게 쓸 수 있기 때문이죠 

MVVM 패턴

MVVM의 문제점은 명확해요

양방향 바인딩 문제:

  • View가 ViewModel의 상태를 직접 변경 가능
  • ViewModel도 상태를 변경 가능
  • 결과: 누가 언제 어디서 변경했는지 추적이 어려워요

SwiftUI 철학과의 충돌 💥:

  • ViewModel에서 SSOT를 관리하면, SwiftUI의 단방향 철학을 무시하게 돼요
  • @State, @Environment 같은 프로퍼티 래퍼가 있는데 사용하지 못하는 상황이 발생해요
  • 오히려 SwiftUI의 강력한 기능들이 불필요해지는 거죠
MVI 패턴

MVI는 사실 안드로이드에서 제안된 패턴이에요. 근데 단방향이기 때문에 SwiftUI와 정말 잘 맞아요.

Intent는 Action이라고 생각하면 돼요. View에서 Action을 보내면 Store가 받아서 그에 맞는 작업을 통해 상태를 변화시켜요.

그러면 그걸 다시 뷰에 업데이트하는 방식이죠.

1. 상태

struct TodoState: Equatable {
    var todos: [Todo] = []

    var filteredTodos: [Todo] {
        // Computed property
    }
}

2. 액션

enum TodoIntent {
    case load
    case add(String)
    case delete(UUID)
}

View가 load되어 intent를 보낼 수 있고, add 버튼을 누를 수 있고... 뷰에서 보낼 수 있는 모든 Action을 명시해놨어요.

 

3 . 상태관리 Store

@MainActor
final class TodoStore: ObservableObject {
    @Published private(set) var state: TodoState
    
    func send(_ intent: TodoIntent) {
        switch intent {
        case .add(let title):
            state = TodoState(
                todos: state.todos + [Todo(title: title)],
                filter: state.filter
            )
        // ... 다른 케이스
        }
    }
}

핵심:

  • private(set) → 읽기만 가능
  • send()로만 상태 변경
  • 매번 새 State 생성 (불변성)

4. View: 순수 렌더링

struct TodoListView: View {
    @StateObject private var store = TodoStore()
    
    var body: some View {
        List {
            ForEach(store.state.filteredTodos) { todo in
                TodoRow(todo: todo) {
                    store.send(.toggle(todo.id))
                }
            }
        }
        .onAppear { store.send(.load) }
    }
}

장점:

  • SwiftUI 철학과 완벽 일치
  • 단방향 흐름 
  • SSOT 보장(구조체로 그때마다 새로 값을 복사하니까)

단점:

  • ❌ 높은 초기 복잡도
  • ❌ 많은 보일러플레이트
  • ❌ 학습 곡선

MVI 철학을 담은 나만의 ViewModel 재정의

최종적으로 생각해본 결과, 결국 단방향을 유지하는 것은 동일하고 Store든 ViewModel이든 이름만 다르다고 판단했어요.

TCA 같은 강제하는 패턴을 제외하고는 전부 본인의 입맛대로 커스텀한다고 생각했어요

Struct State의 고민

상태를 Struct로 관리했을 때 장점은 명확해요. 사이드 이펙트가 없고, 값 타입이라 Thread-safe하죠. 하지만 단점도 있어요.

만약 상태에서 크기가 큰 데이터들을 저장하고 있을 때:

struct HugeState {
    var users: [User] // 10,000개
    var posts: [Post] // 50,000개
    var comments: [Comment] // 100,000개
}

한 개만 바뀌더라도 전체를 복사해야 하는데, 자주 변경되는 데이터면 복사 비용이 더 들지 않을까요?

거기다 보일러플레이트 코드 양도 많아지고요.

 

그래서 다르게 접근해보았습니다:)

protocol UserProfileViewActions {
    func loadProfile() async
    func updateUsername(_ newName: String) async
}

@Observable
final class UserProfileViewModel: UserProfileViewActions {
    // MARK: - 상태 프로퍼티 (읽기 전용)
    private(set) var profile: UserProfile?
    private(set) var isLoading = false
    private(set) var errorMessage: String? = nil
    
    // MARK: - Actions (단방향 진입점)
    func loadProfile() async {
        isLoading = true
        // ... UseCase 호출
        isLoading = false
    }
    
    func updateUsername(_ newName: String) async {
        // ... UseCase 호출 후 상태 업데이트
    }
}
  • Protocol로 Action 명시: 사용자의 모든 행동을 protocol로 받아 명확하게 드러냈어요
  • private(set)으로 단방향 보장: 쓰기를 제한하는 것만으로도 충분히 예측 가능하다고 생각했어요
  • Class로 성능 확보: 상태가 변하더라도 복사 비용 없이 빠르게 개발할 수 있어요

특히 상태가 여러 뷰에서 공유되지 않는 작은 프로젝트에서는, 현재처럼 ViewModel에 국한된 상태만 관리하면 되니까 Struct의 복사 비용을 감수할 필요가 없다고 판단했어요.

'iOS' 카테고리의 다른 글

URLSession 한 줄 뒤에 숨겨진 것들: DNS부터 전자기파까지  (2) 2025.10.11
HTTP 캐싱(Etag & max-age) 그리고 iOS에서는?  (0) 2025.10.01
URLSession에 대한 에브리띵  (0) 2025.09.27
스크롤뷰는 어떻게 스크롤될까? 🤔 UIView 렌더링부터 시작하는 완벽 분석  (0) 2025.09.26
데이터 보관은 어떻게이루어질까(직렬화: NSCoding부터 Codable까지)  (0) 2025.09.09
'iOS' 카테고리의 다른 글
  • URLSession 한 줄 뒤에 숨겨진 것들: DNS부터 전자기파까지
  • HTTP 캐싱(Etag & max-age) 그리고 iOS에서는?
  • URLSession에 대한 에브리띵
  • 스크롤뷰는 어떻게 스크롤될까? 🤔 UIView 렌더링부터 시작하는 완벽 분석
2료일
2료일
좌충우돌 모든것을 다 정리하려고 노력하는 J가 되려고 하는 세미개발자의 블로그입니다. 편하게 보고 가세요
  • 2료일
    GPT에게서 살아남기
    2료일
  • 전체
    오늘
    어제
    • 분류 전체보기 (133) N
      • SWIFT개발일지 (28)
        • ARkit (1)
        • Vapor-Server with swift (3)
        • UIkit (2)
      • 알고리즘 (25)
      • Design (6)
      • iOS (42) N
        • 반응형프로그래밍 (12)
      • 디자인패턴 (6)
      • CS (3)
      • 도서관 (2)
  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
2료일
UIKit / SwiftUI에서는 무슨 디자인 패턴을 사용해야할까?
상단으로

티스토리툴바