MVVM은 UI 및 비 UI 코드를 분리하기 위한 디자인 패턴이다.
이는 프로그램의 복잡성을 줄이고 서로 다른 관심사를 각 영역으로 분리함으로써 관리성과 유지 보수성을 향상시킨다.
MVVM은 사용자 인터페이스 로직을 비즈니스 로직과 분리함으로써 사용자 인터페이스의 개발과 테스트를 보다 쉽게 만들어준다.
사용자 인터페이스를 개발하고 테스트하기 쉽도록 사용자 인터페이스 로직을 비즈니스 로직과 분리한다.
MVVM 패턴의 주요 구성 요소는 Model, View, ViewModel의 세 부분으로 이루어져 있다.
이는 데이터 및 비즈니스 로직을 나타낸다.
모델은 데이터베이스에 접근하거나, 네트워크에서 데이터를 받아오거나, 유효성 검사 등의 비즈니스 로직을 수행한다.
이는 사용자에게 보여지는 부분으로, 사용자 인터페이스와 관련된 모든 것을 담당한다.
뷰는 사용자의 입력을 받고, 데이터를 보여주며, 사용자에게 다양한 인터랙션을 제공힌다.
이는 뷰와 모델 사이의 연결 고리 역할을 한다.
데이터 바인딩과 명령 패턴(Command Pattern)이라는 두 가지 핵심 기술을 사용는데,
데이터 바인딩은 ViewModel의 데이터 변경이 자동으로 View에 반영되고, 명령패턴은 View의 변경 사항이 ViewModel에 자동으로 업데이트되는 것을 의미한다. 이를 통해 View와 ViewModel 사이의 동기화를 자동으로 유지합니다.
command 패턴은 사용자의 액션(예: 버튼 클릭, 텍스트 입력 등)을 추상화하여, ViewModel이 이러한 액션을 캡슐화하고 관리할 수 있게 한다. 이를 통해 ViewModel은 View에 대해 알지 못하며, 단지 액션에 반응하기만 하면 된다.
이 방식은 View와 ViewModel 사이의 결합도를 최소화하고 테스트 용이성을 높이는 데 도움이 된다.
사용자 인터페이스(UI) 요소가 애플리케이션의 데이터 소스와 '바인딩'되는 것을 의미한다.
이를 통해 UI 요소는 데이터 변경을 실시간으로 반영하며, 사용자의 조작 등으로 인한 데이터 변경에 따른 UI 상태 변경도 즉시 반영되는 효과를 얻을 수 있다.
MVVM에서 데이터 바인딩은 핵심적인 역할을 한다. ViewModel의 데이터 상태가 변경될 때, 이 변경사항이 자동으로 View에 반영되어야 하는데, 이를 'One-Way Data Binding'(단방향 데이터 바인딩)이라고 한다. 또한, 반대로 사용자의 조작 등으로 View의 상태가 변경될 경우, 이 변경사항이 ViewModel의 데이터 상태에 자동으로 반영되어야 하는데, 이를 'Two-Way Data Binding'(양방향 데이터 바인딩)이라고 한다.
데이터 바인딩을 통해 View와 ViewModel 사이의 데이터 동기화를 자동으로 유지할 수 있다.
이런 특성 때문에, MVVM 패턴을 지원하는 프레임워크(Angular, Vue.js 등)은 대부분 데이터 바인딩 기능을 제공한다.
MVVM(Model-View-ViewModel) 패턴에서는 주로 양방향(Two-Way) 데이터 바인딩을 사용한다.
양방향 데이터 바인딩은 View의 변경이 ViewModel에 반영되고, ViewModel의 변경이 View에 반영되는 방식을 의미한다.
이는 ViewModel의 상태가 변경될 때 이를 View가 즉시 반영하도록 하며, 반대로 사용자의 액션에 의해 View의 상태가 변경될 때 ViewModel의 상태가 자동으로 업데이트되도록 하는 것을 가능하게 한다.
따라서 MVVM 패턴에서는 사용자의 입력과 같은 View의 상태 변화를 ViewModel이 추적하고, 이를 Model에 반영하며, 동시에 Model의 변경사항을 ViewModel이 받아와 View에 반영하는 등, 양방향의 동적인 상호작용이 일반적이다.
하지만, 실제 애플리케이션의 요구사항과 상황에 따라 양방향 데이터 바인딩 대신 단방향 데이터 바인딩(One-Way Data Binding)을 사용할 수도 있다. 단방향 데이터 바인딩은 ViewModel의 변경만 View에 반영되는 방식으로, 이 경우에는 View에서 발생하는 변경이 ViewModel에 직접적으로 반영되지 않는다. 이런 방식은 데이터 흐름을 좀 더 명확하게 만들어 주기 때문에 복잡한 애플리케이션에서 상태 관리를 좀 더 편리하게 할 수 있다.
다음은 리액트로 간단하게 MVVM 구조를 따라 작성해본 예시코드이다.
import React, { useReducer, useContext, createContext } from 'react';
// Model
const initialTodos = [
{ id: 1, title: 'Get Coffee', finished: false },
{ id: 2, title: 'Write simpler UIs', finished: false }
];
// ViewModel: 액션 디스패칭을 통해 커맨드 패턴을 구현한 리듀서
function todoReducer(state, action) {
switch (action.type) {
case 'add':
return [...state, { id: Date.now(), title: action.title, finished: false }];
case 'toggle':
return state.map(todo =>
todo.id === action.id ? { ...todo, finished: !todo.finished } : todo
);
default:
throw new Error();
}
}
// ViewModel
const TodoContext = createContext();
function TodoProvider({ children }) {
const [state, dispatch] = useReducer(todoReducer, initialTodos);
// 데이터 바인딩의 일부로서 상태와 디스패치 함수를 컨텍스트로 제공하는데,
// 이를 통해 View에서 상태를 전달하고 조작할 수 있다.
return (
<TodoContext.Provider value={{ state, dispatch }}>
{children}
</TodoContext.Provider>
);
}
// View
function TodoListView() {
const { state, dispatch } = useContext(TodoContext);
// 데이터 바인딩: 컨텍스트로부터 상태와 디스패치 함수를 가져와서 활용하는 과정
return (
<div>
{state.map(todo => (
<div key={todo.id}>
<input
type="checkbox"
checked={todo.finished}
// 커맨드 패턴: 버튼 클릭에 대한 액션을 'toggle' 커맨드로 캡슐화
onChange={() => dispatch({ type: 'toggle', id: todo.id })}
/>
{todo.title}
</div>
))}
</div>
);
}
function App() {
return (
<TodoProvider>
<TodoListView />
</TodoProvider>
);
}
export default App;
위의 React 코드에서도 단방향 데이터 바인딩이 이루어지고 있다.
여기서 상태(state)는 TodoProvider 컴포넌트에서 관리되고 있으며, 이 상태는 TodoContext를 통해 TodoListView 컴포넌트로 전달되어진다.
그러나 코드에서 볼 수 있듯이, dispatch 함수를 통해 View에서 발생하는 이벤트를 통해 상태를 변경하는 작업이 이루어지는데,
이를 통해 ViewModel과 View 사이의 '가상의' 양방향 데이터 바인딩을 구현하였다고 볼 수 있다.
체크박스의 상태 변경을 감지하는 이벤트 핸들러에서 dispatch 함수를 호출하여 상태를 변경하고, 이 변경된 상태가 다시 View에 반영되는 구조이다.
따라서 이것은 React에서 '양방향 데이터 바인딩 같은 효과'를 내는 하나의 방식이다.
그러나 이는 여전히 React의 단방향 데이터 흐름 구조 내에서 이루어지는 것이므로, 순수한 의미에서의 양방향 데이터 바인딩이라고 보기는 어렵다.
이는 ViewModel의 상태가 변경되면 이를 View에 바로 반영하되, 반대로 View의 변경을 ViewModel에 바로 반영하는 구조인 진정한 양방향 데이터 바인딩과는 다르다.
Flux는 Dispatcher, Store, View, Action의 네 가지 주요 구성 요소를 갖지만, MVVM은 Model, View, ViewModel의 세 가지 구성 요소를 갖는다.
Flux에서는 데이터 흐름이 단방향인데, View에서 사용자 입력을 받아 Action을 생성하고, 이 Action이 Dispatcher에 전달되며, Dispatcher는 이 Action을 등록된 모든 Store에 전달한다. 이후 Store가 업데이트되면, 이 변경 사항이 View에 반영된다. 따라서, Flux에서는 명시적인 업데이트 메커니즘이 필요하다.
MVVM에서는 데이터 바인딩 기술을 사용하여 View와 ViewModel 사이에 양방향 통신을 가능하게 한다.
이는 ViewModel의 데이터가 변경될 때 View가 자동으로 업데이트되며, 반대로 사용자 입력이 ViewModel을 자동으로 업데이트 한다.
따라서 MVVM에서는 업데이트가 암시적으로 처리된다.
Flux 패턴에서는 View와 Store 사이에 직접적인 의존성이 없다. View는 단지 Dispatcher에서 이벤트를 받아 처리한다.
반면 MVVM 패턴에서는 ViewModel이 View와 Model 사이의 연결 역할을 하며, View와 ViewModel 사이에는 데이터 바인딩을 통한 의존성이 존재한다.
제시한 React 코드는 MVVM에 가까운 이유는, Flux 패턴에서 볼 수 있는 Dispatcher나 여러 Store의 개념이 없기 때문이다. 대신, useContext와 useReducer를 통해 View와 ViewModel 사이에 양방향으로 데이터를 주고 받으며, ViewModel이 상태와 로직을 관리하는 형태를 볼 수 있다. 이러한 모습은 MVVM 패턴에서 ViewModel이 가지는 역할에 더 가까워 보인다.
++
일반적으로 ViewModel은 View에 대한 모든 비즈니스 로직과 상태를 관리하는 역할을 하므로, 하나의 View에 대해 하나의 ViewModel이 연결되는 1:1 관계가 이상적일 수 있다. 이렇게 하면 View와 ViewModel 사이의 관심사가 분리되어 코드의 가독성과 유지 관리성이 향상된다.
그러나 실제 애플리케이션에서는 여러 View가 동일한 로직이나 상태를 공유해야 하는 경우가 자주 발생한다. 이런 경우에는 하나의 ViewModel이 여러 View에 연결되는 1:n 또는 n:1 관계를 가질 수 있다.
따라서, MVVM 패턴에서 View와 ViewModel의 관계는 애플리케이션의 복잡성, 요구 사항, 개발 방식 등에 따라 달라질 수 있으며, 항상 1:1 관계일 필요는 없다.
장점
분리된 관심사: MVVM 패턴에서는 Model, View, ViewModel의 역할이 분리되어 있다.
이를 통해 각 구성 요소의 책임이 명확해지고 코드의 가독성과 유지 보수성이 향상된다.
데이터 바인딩: 데이터 바인딩을 통해 View와 ViewModel 사이의 상호 작용이 자동화되고,
이를 통해 UI 코드를 간소화하고 오류를 줄일 수 있다.
테스트 용이성: ViewModel은 View로부터 분리되어 있으므로, UI 없이 비즈니스 로직을 테스트하는 것이 가능하다.
코드 재사용: ViewModel은 View에 종속되지 않기 때문에, 여러 View에서 재사용이 가능하다.
복잡성 증가: MVVM 패턴은 구조가 복잡하며, 이로 인해 프로젝트의 복잡성이 증가할 수 있다.
초기 학습 곡선: MVVM 패턴의 이해와 구현에는 초기 학습 곡선이 필요하다.
개발자들이 패턴의 원리와 사용 방법을 완전히 이해하는 데 시간이 필요할 수 있다.
성능 문제: 데이터 바인딩에 의존하는 경우, 많은 수의 바인딩이 발생하면 애플리케이션의 성능에 부정적인 영향을 미칠 수 있다.