MVI(Model-View-Intent) 패턴은 상태(State) 관리를 통해 UI를 업데이트하는 아키텍처다. 단방향 데이터 흐름이 특징이며, SwiftUI 같은 선언형 UI 프레임워크에 걸맞다.
여기서 Intent의 사전적인 의미는 의도인데, 사용자 상호작용의 의도를 말한다. 의도에 따라 데이터가 변하고, 그에 따라 변하는 상태가 UI를 그리는 흐름이라고 보면 된다.

MVI 아키텍처 다이어그램
출처 - 나. 내가 그림.
MVI의 흐름은 다음과 같다.
View는 사용자의 상호작용(User Event)을Intent로 변환한다.Intent는 상호작용의 의도에 따라Model이 데이터를 처리(Process)하도록 한다.Model은 처리 결과를 새로운 상태(State)로 갱신(Update)한다.- 갱신된
State를 바탕으로View가 렌더링(Render)된다.
State는 앱의 현재 상태를 표현하는 구조체로, UI를 표현할 순수 데이터를 포함한다.
struct MainState {
var todos: [Todo] = []
var error: DataError?
var hasError: Bool {
error != nil
}
}
View는 State를 보관하는 Store 객체를 통해 State에 접근하여 UI를 그린다.
struct MainView: View {
@StateObject private var store: MainStore
private let repository: TodoRepository
init(repository: TodoRepository) {
self.repository = repository
_store = StateObject(wrappedValue: MainStore(repository: repository))
}
var body: some View {
// ... //
// "Todo" List : MainState.todos로 렌더링
List {
ForEach(store.state.todos) { todo in
Text(todo.text)
}
}
// error 발생 시 alert : MainState.hasError, error 값으로 렌더링
.alert("Error", isPresented: .constant(store.state.hasError), actions: {
Button("OK") {
if let error = store.state.error {
print(error.errorDescription)
}
}
}, message: {
Text("Data could not be loaded. Please try again later.")
})
}
}
Intent는 User Event가 의도하는 액션을 가리키므로, Action이라는 이름으로 정의되기도 한다. 인터랙션(상호작용) 발생 시 Store의 send 메소드로 Reducer에 전달되어 필요한 데이터 처리를 유도한다. (Reducer는 하나의 객체가 아니라 함수다. 상태를 업데이트하는 역할과 책임을 가졌기 때문에 이를 나타내기 위한 개념적인 용어다.)
enum MainAction {
case createTodo(input: String)
case deleteTodo(id: UUID)
}
struct MainView: View {
@StateObject private var store: MainStore
private let repository: TodoRepository
init(repository: TodoRepository) {
self.repository = repository
_store = StateObject(wrappedValue: MainStore(repository: repository))
}
@State private var textFieldText: String = ""
var body: some View {
// ... //
List {
ForEach(store.state.todos) { todo in
Text(todo.text)
// 스와이프 시 todo 삭제를 의도
.swipeActions {
Button {
store.send(.deleteTodo(id: todo.id)
} label: {
Image(systemName: "trash")
}
}
}
}
Button {
// 버튼을 눌렀을 때 todo 생성을 의도
store.send(.createTodo(input: textFieldText))
} label: {
Text("Create")
}
}
}
Store는 Intent를 받아 Reducer를 통해 액션을 처리하고, 그에 따른 상태(State) 변경을 관리하는 객체로, MVI 아키텍처에서 중심 역할을 맡는다. View는 Store를 구독하여 상태 변화에 따라 자동으로 UI를 갱신하게 된다. 어떻게 보면 MVVM에서의 ViewModel의 역할과 비슷한데, Intent/Reducer/State의 명확한 역할 분리로 그보다 좀더 구조화된 객체라고 보면 된다.
final class MainStore: ObservableObject {
private let repository: TodoRepository
private var cancellables = Set<AnyCancellable>()
init(repository: TodoRepository) {
self.repository = repository
bind()
}
@Published private(set) var state = MainState()
private func bind() {
Publishers.CombineLatest(
repository.$todos,
repository.$error
)
.receive(on: DispatchQueue.main)
.sink { [weak self] todos, error in
// 상태(State) 업데이트
self?.state = MainState(todos: todos, error: error)
}
.store(in: &cancellables)
}
// view에서 호출되어 Action(Intent)을 Reducer로 전달
func send(_ action: MainAction) {
reduce(action)
}
// Reducer : Action 처리
private func reduce(_ action: MainAction) {
switch action {
case .createTodo(let input):
repository.createTodo(with: input)
case .deleteTodo(let id):
repository.deleteTodo(with: id)
}
}
}