Vue & MVVM Pattern

What is Vue?


Vue.js 공식사이트에서는 Vue.js를 ‘점진적인 JavaScript 프레임워크’(The Progressive JavaScript Framework)라 소개한다. Vue.js의 코어 라이브러리는 React와 유사하게 데이터 바인딩과 컴포넌트에만 집중한다. 그러나 복잡한 대규모 애플리케이션을 개발하려면 라우팅, 상태 공유, 컴포넌트 간의 통신 등을 위해 별도의 수많은 도구가 필요해진다. Ember나 Angular는 이런 도구를 프레임워크에 내재하는 형태로 접근한다. React는 도구를 제공하는 역할을 커뮤니티를 통한 생태계에 맡기고 있다.

Vue.js는 중간적인 형태로 접근을 한다. 코어는 최소한의 기능만 제공하고, 필요한 다른 도구는 별도로 제공한다. Vue.js의 도구는 모두 공식적으로 관리되고 완성도 높은 문서가 함께 제공된다.

Vue
출처: Vue

위에서 Vue.js는 프레임워크라고 말했지만, ‘점진적인’ 말에 주목해야 한다. 즉, Vue.js는 작은 화면단 라이브러리 역할부터 큰 규모의 웹 애플리케이션 개발을 돕는 프레임워크 역할까지 점진적으로 적용할 수 있는 프론트엔드 프레임워크를 말한다. Vue.js는 MVVM 패턴에서 View와 Model을 연결해주는 ViewModel 계층에 초점을 둔 프레임워크이다. 뷰모델을 통해서 양방향 데이터 바인딩이 가능하게 해주며, 뷰 계층을 좀 더 간단하고 유연하게 디자인하게 해준다. 즉, 모델과 뷰의 동기화 역할을 한다.


MVVM Pattern


MVVM 패턴이란, Mode - View - ViewModel의 줄임말로, 로직과 UI의 분리를 위해 설계된 패턴이다. 웹페이지는 돔과 자바스크립트로 만들어지게 되는데 돔이 View 역할을 하고, 자바스크립트가 Model 역할을 한다. 뷰모델이 없는 경우에는 직접 모델과 뷰를 연결해야 한다. 그러나 뷰모델이 중간에서 연결해 주는 것이 MVVM 모델이다.

MVVM Pattern
출처: MVVM Pattern

사용자 인터페이스에 해당하는 뷰(View)와 뷰에 표시되는 데이터(Model) 그리고 뷰(View)와 모델(Model) 사이에서 여러 비즈니스 로직을 처리하는 뷰-모델(View-Model)로 분리하여, 유지보수를 쉽게하고 뷰(View) 처리에 다양한 기능을 사용할 수 있게 되었다.

MVVM Pattern을 총 3개로 나눌 수 있다.

  • Model (비즈니스 규칙, 데이터 접근, 모델 클래스)
  • View (사용자 인터페이스)
  • ViewModel (모델과 뷰 사이의 인터페이스)

뷰모델(ViewModel)은 모델(Model)과 뷰(View)사이에 인터페이스 역할을 한다.
뷰모델은 모델의 데이터를 뷰에 바인딩하고, 명령어를 사용하여 모든 UI의 동작들을 다룬다.
뷰는 뷰모델의 프로퍼티에 제어값을 바인딩하며 차례대로 모델 객체에 있는 데이터를 노출시킨다.

예를 들어 사용자가 뷰에 있는 계산기 프로그램을 사용하여 결과값을 요청하는 버튼을 클릭했을 때, 뷰모델이 요청받은 동작을 수행한다. 뷰모델의 연산 기능을 담당하는 명령어는 연산 후 모델의 데이터 값을 변경시킨다. 만약 뷰모델의 속성값이 변경되면(모델의 데이터 값이 변경되면) 새로운 속성 값들은 데이터 바인딩(data binding)과 알림(notification)을 통해 자동적으로 뷰에 적용된다.

간단한 코드로 보면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
// View
<div id = “app”
{{message}}
</div>
</template>


<script>
// Model
var model = {
message : “뷰 생성"
}

// ViewModel
new Vue({
el : ‘#app’,
data : model
})

</script>

MVVM Pattern
출처: MVVM Pattern


Todo 앱으로 MVVM Pattern으로 개발하기


먼저, 부모 역할을 하는 Todolist.vue가 있는데, 여기서 Todo List App을 클릭하면, 라우터를 통해 홈(‘/‘)으로 이동한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<template>
<div id="todolistapp">
<div id="header" class="header">
<router-link to='/'><h2>Todo List App</h2></router-link>
<router-link to='/note'><p>Note</p></router-link>
</div>

<br>

<router-view></router-view>
</div>
</template>

<script type="text/javascript">

export default {
name : 'todo-list',
}
</script>

아래는, Todo를 추가할 수 있는 Todo.vue인데, 여기서는 InputTodo.vue와 List.vue를 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>

<input-todo />

<br>
<br>

<list></list>
</div>
</template>

<script>
import InputTodo from './InputTodo';
import List from './List';

export default {
name : 'todo',
components : { InputTodo, List }
}

아래는, InputTodo.vue이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
// View
<div>
<input class="input" type="text" id="task" v-model="todo"
placeholder="입력 후 엔터!" v-on:keyup.enter="addTodo">
<span class="addbutton" v-on:click="addTodo">추 가</span>
</div>
</template>

<script type="text/javascript">
import eventBus from '../EventBus'

// ViewModel
export default {
name : 'input-todo',
// Model
data : function() {
return { todo : "" }
},
methods : {
addTodo : function() {
console.log(this.todo);
eventBus.$emit('add-todo', this.todo);
this.todo = "";
}
}
}
</script>

아래는, List.vue이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
<template>
// View
<ul id="todolist">
<li v-for="a in todolist" :key="a.id" :class="checked(a.done)"
@click="doneToggle(a.id)">
<span>{{ a.todo }}</span>
<span v-if="a.done"> (완료)</span>
<span class="close" v-on:click.stop="deleteTodo(a.id)">&#x00D7;</span>
</li>
</ul>
</template>

<script type="text/javascript">
import eventBus from '../EventBus'

// ViewModel
export default {
created : function() {
eventBus.$on('add-todo', this.addTodo);
},
// Model
data : function() {
return {
todolist : [
{ id:1, todo : "vue", done:false },
{ id:2, todo : "react", done:true },
{ id:3, todo : "python", done:false },
{ id:4, todo : "java", done:false },
]
}
},
methods : {
checked : function(done) {
if(done) return { checked:true };
else return { checked:false };
},
addTodo : function(todo) {
if (todo !== "") {
this.todolist.push(
{ id:new Date().getTime(), todo : todo, done:false });
}
},
doneToggle : function(id) {
var index = this.todolist.findIndex((item)=>item.id === id);
this.todolist[index].done = !this.todolist[index].done;
},
deleteTodo : function(id) {
var index = this.todolist.findIndex((item)=>item.id === id);
this.todolist.splice(index,1);
}
}
}
</script>