Vue.js를 이용한 Todo리스트 예제 만들기(2)
저번 포스트에 이어서 카테고리, 검색어 입력을 통한 검색 기능/보기 방식 변경/정렬 방식 변경/완료한 항목 체크 기능을 구현하면서 사용한 vue 기능을 정리해보려고 합니다.
3. 카테고리, 검색어 입력을 통한 검색 기능
* 결과 화면
* html(form부분만)
<form class="s-form">
<select v-model="option" class="form-group" v-on:change="filter">
<option value="all">All</option>
<option v-for="c in categories" v-bind:value="c">{{ c }}</option>
</select>
<input v-model="search" v-on:keyup="filter" type="text" placeholder="search..." class="form-group">
</form>
* js(데이터&사용한 함수 부분만)
var app = new Vue({
el: "#app",
data: {
option: 'all',
search: '',
categories: [],
todos: [],
viewType: 'list',
sortType: 'sort-numeric-up',
buttons: {
'view': [
{'class':'list', 'title':'view in list', selected:true},
{'class':'th-large', 'title':'view in thumbnail', selected:false}
],
'sort': [
{'class':'sort-numeric-up', 'title':'sort by deadline', selected:true},
{'class':'sort-alpha-down', 'title':'sort by alphabet', selected:false},
{'class':'star', 'title':'sort by stars', selected:false}
]
}
},
methods: {
filter: function(){
var isFiltered = false;
var todoObj = {};
for(var i = 0; i < this.todos.length; i++){
isFiltered = false; // init
todoObj = this.todos[i];
// -- filter by category
if(this.option !== 'all' && todoObj.category !== this.option){
isFiltered = true;
}
// -- filter by search keyword
if(!isFiltered && todoObj.title.toUpperCase().indexOf(this.search.toUpperCase()) == -1){
isFiltered = true;
}
if(isFiltered){
todoObj.filtered = "y";
} else {
todoObj.filtered = "n";
}
}
}
검색 조건이 복잡하지 않고, 항목이 많지 않아서 재조회를 하는 대신에 검색 조건에 맞지 않는 항목을 필터링해서 v-show 지시어를 사용해서 화면에서 잠시 감춰주는 방식으로 구현하였습니다. 검색 버튼을 별도로 두지 않고 select 태그에서 change 이벤트가 발생할 때, input 태그에서 keyup 이벤트가 발생할 때 바로 검색을 하도록 구현해보았습니다. filter라는 함수가 입력한 검색 조건에 맞지 않는 항목을 필터링하는 역할을 합니다.
3-1. v-model
v-model 지시어는 form 요소에 데이터 바인딩을 할 때 사용합니다. 위의 예제에서는 select 태그에 option 데이터를 바인딩하고, input 태그에 search 데이터를 바인딩하였습니다. 이렇게 하면 초기에 렌더링할 때 해당 데이터의 value가 각 태그의 value로 바인딩되는 것을 확인할 수 있습니다. 그 후 화면에서 select의 옵션을 바꾸거나 input 태그에 글자를 입력하면 데이터도 사용자가 선택/입력한 것으로 업데이트 됩니다.
v-model 지시어를 통해 form 요소에 사용자가 선택/입력한 value로 데이터를 업데이트 하면, filter라는 함수에서 이 데이터를 받아 기존 todo 리스트에서 카테고리 혹은 타이틀이 검색 조건과 맞지 않는 요소를 걸러내는 작업을 합니다.
3-2. Reactive Data
이 부분을 구현하다가 주의할 점 하나를 발견했습니다. 반응하는 데이터가 되기 위해서는 vue 인스턴스를 생성할 때부터 있는 데이터여야 한다는 점입니다. 쉽게 전달하기 위하여 제 경우를 들어 설명해보겠습니다. 맨 처음에 todo 리스트 아이템이 가지고 있던 속성은 다음과 같았습니다.
{
"thumbnail": "/data/images/vegetables.jpg",
"category": "shopping",
"title": "Buying some healthy food",
"startDate": "20180310152031",
"deadline": "20180315180000",
"endDate": "",
"important": "3"
}
추후 검색어를 입력하거나 카테고리를 선택할 때 실행되는 filter 메소드에서 todo 리스트 아이템 객체에 filtered라는 속성을 추가해줄 생각이었습니다. 그래서 마크업에서는 리스트 항목이 보일 지 정하는 조건을 v-show="item.filtered!=='y'"라고 작성하였습니다. 그런데 이게 작동을 하지 않았습니다. 분명 콘솔에 찍어봤을 때는 객체가 filtered라는 속성을 갖고 있음에도 불구하고 작동하지 않았습니다. 왜냐면 처음에 데이터를 세팅할 당시에는 객체에 filtered라는 속성이 없었기 때문입니다. 결론적으로, 저는 샘플 데이터 json파일에서 각 아이템에 filtered라는 속성을 추가해줬습니다.
이 부분과 관련해서 vue js 가이드 문서에도 명확히 언급하고 있습니다. '나중에 추가한 property는 화면 업데이트를 촉발시키지 않을 것이다. 만약 추후에 사용할 속성이라면 처음부터 어떤 초기 값을 세팅해야 한다.' 라고 말이죠.(vue 가이드 문서-Data and Methods)
4. 보기 방식 변경&정렬 방식 변경
* 결과 화면
* html(전체)
<div id="app">
<form class="s-form">
<select v-model="option" class="form-group" v-on:change="filter">
<option value="all">All</option>
<option v-for="c in categories" v-bind:value="c">{{ c }}</option>
</select>
<input v-model="search" v-on:keyup="filter" type="text" placeholder="search..." class="form-group">
</form>
<div class="content">
<div class="button-group">
<div class="buttons">
<span>view</span>
<button v-for="b in buttons.view" v-bind:title="b.title"
v-bind:class="viewType == b.class?'selected':''"
v-on:click="toggleList('view',b.class)">
<i class="fas" v-bind:class="'fa-'+b.class"></i>
</button>
</div>
<div class="buttons">
<span>sort</span>
<button v-for="b in buttons.sort" v-bind:title="b.title"
v-bind:class="sortType == b.class?'selected':''"
v-on:click="toggleList('sort',b.class)">
<i class="fas" v-bind:class="'fa-'+b.class"></i>
</button>
</div>
</div>
<ul class="todo-list list" v-if="viewType === 'list'">
<li v-for="(item,index) in todos"
v-bind:class="[substring(item.deadline, 0, 8) == getTodayDate? 'deadline':'', item.endDate? 'done':'']"
v-show="item.filtered!=='y'"
v-on:click="checkTodo(index)">
<i class="far" v-bind:class="item.endDate? 'fa-check-square':'fa-square'"></i>
<span class="title">{{ item.title }}</span>
<span class="stars">
<i v-for="n in toNumber(item.important)" class="fas fa-star"></i>
</span>
</li>
</ul>
<ul class="todo-list thumbnail" v-else>
<li v-for="(item,index) in todos" v-show="item.filtered!=='y'"
v-bind:class="item.endDate? 'done':''" v-on:click="checkTodo(index)">
<div class="label">
<span class="category">{{item.category}}</span>
<span class="stars">
<i v-for="n in toNumber(item.important)" class="fas fa-star"></i>
</span>
</div>
<img v-bind:src="item.thumbnail">
<p class="info">
<span class="title">{{ item.title }}</span>
<span class="deadline">
<i class="far" v-bind:class="item.endDate? 'fa-check-square':'fa-square'"></i>
until {{ formatDate(item.deadline) }}
<i v-if="substring(item.deadline, 0, 8) == getTodayDate" class="fas fa-bell"></i>
</span>
</p>
</li>
</ul>
</div>
</div>
* js(보기 방식 변경, 정렬 방식 변경 관련 일부)
var app = new Vue({
el: "#app",
data: {
option: 'shopping',
search: '',
categories: [],
todos: [],
viewType: 'list',
sortType: 'sort-numeric-up',
buttons: {
'view': [
{'class':'list', 'title':'view in list'},
{'class':'th-large', 'title':'view in thumbnail'}
],
'sort': [
{'class':'sort-numeric-up', 'title':'sort by deadline'},
{'class':'sort-alpha-down', 'title':'sort by alphabet'},
{'class':'star', 'title':'sort by stars'}
]
}
},
methods: {
toggleList: function(type, className){
switch (type){
case 'view':
this.viewType = className;
break;
case 'sort':
this.sortType = className;
if(className === 'sort-numeric-up')
this.todos.sort(function(a,b){return a['deadline']>b['deadline']});
else if(className === 'sort-alpha-down')
this.todos.sort(function(a,b){return a['title']>b['title']});
else if(className === 'star')
this.todos.sort(function(a,b){return a['important']<b['important']});
break;
}
},
...
버튼 마크업은 static으로 하지 않고, vue 데이터에서 buttons 속성으로 보기 방식과 관련된 버튼 정보는 view라는 이름의 배열로, 정렬과 관련된 버튼 정보는 sort라는 이름의 배열로 구성하여, 각각 이 항목만큼 반복을 하여 버튼이 렌더링 되도록 구성하였습니다. 버튼의 개수만큼 직접 태그를 작성하면, 태그마다 조건에 따른 클래스의 변경과 관련한 v-bind:class 속성과 클릭 이벤트 속성인 v-on:click, v-bind:title 속성을 각 요소마다 똑같이 적어야 하는 번거로움이 발생하는데, 이 점을 피하고 싶었습니다.
4-1. 클래스 toggle하기
화면 개발을 하다보면 사용자가 선택한 탭이나 버튼의 스타일을 변경하는 처리를 자주 하게 됩니다. 이 때는 선택된 요소만이 갖는 클래스에 스타일을 정하여, 선택한 요소에는 해당 클래스를 부여하고 이전에 클래스를 가지고 있던 요소에서는 클래스를 삭제하여 처리를 하곤 합니다. 이렇게 클래스를 토글하는 것도 vue를 사용하면 아주 간단하게 처리할 수 있습니다.
jQuery를 사용하여 처리할 때는 기존 요소를 찾아 removeClass()를 통해 클래스를 지워주고, 선택된 요소를 찾아 addClass()로 클래스를 부여하는 식으로 처리를 합니다. 어렵지는 않지만 선택자를 통해 이전에 선택된 요소와 새로 선택된 요소를 찾아야 하므로 조금 번거롭게 느껴졌습니다.
vue를 사용한 위의 예제에서는 data에서 정의한 viewType과 버튼 data가 가지고 있는 'class'라는 값과 비교하여 같은 경우 선택되었다는 의미의 'selected'라는 클래스를 부여하도록 버튼 태그에 조건을 부여하였습니다. 그리고 버튼을 클릭했을 때, toggleList 함수에서 인자로 넘긴 버튼 자신의 class값으로 viewType 데이터를 업데이트 해줍니다. 그러면 데이터의 업데이트를 감지하여 클래스가 toggle됩니다.
4-2. 리스트 정렬방식 변경하기
리스트 정렬방식 변경도 vue를 사용하면 아주 간단하게 처리할 수 있습니다. 정렬 방식 변경을 위해서 기존의 노드를 삭제하거나 노드 순서 변경 등의 노드 조작을 할 필요가 없습니다. 단지 해야할 일은 todo 리스트를 구성하는 todos 라는 배열의 순서를 변경해주는 것입니다. vue가 데이터의 업데이트를 감지하여 화면에서 리스트를 다시 구성해줍니다.
this.todos.sort(function(a,b){return a['deadline']>b['deadline']});
else if(className === 'sort-alpha-down')
this.todos.sort(function(a,b){return a['title']>b['title']});
else if(className === 'star')
this.todos.sort(function(a,b){return a['important']<b['important']});
정렬 방식 변경을 위해서 실질적으로 작업한 부분은 배열 순서를 key값에 따라 정렬하는 이 로직 뿐입니다.
5. 완료한 항목 체크
* 결과 화면
* html(리스트 아이템 부분만)
<li v-for="(item,index) in todos"
v-bind:class="[substring(item.deadline, 0, 8) == getTodayDate? 'deadline':'', item.endDate? 'done':'']"
v-show="item.filtered!=='y'"
v-on:click="checkTodo(index)">
<i class="far" v-bind:class="item.endDate? 'fa-check-square':'fa-square'"></i>
<span class="title">{{ item.title }}</span>
<span class="stars">
<i v-for="n in toNumber(item.important)" class="fas fa-star"></i>
</span>
</li>
* js(체크 부분만)
methods: {
checkTodo: function(index){
var todoObj = this.todos[index];
if(todoObj.endDate){
todoObj.endDate = '';
} else {
var d = new Date();
this.todos[index].endDate = this.getTodayDate + d.toTimeString().replace(/[^0-9]/g, '').substr(0, 6);
}
},
...
}
항목을 체크했을 때 체크박스에 체크가 되면서 글자 가운데 선을 넣는 것도 위에서 사용한 class toggle 방법을 사용하였습니다. 우선 리스트 아이템을 구성하는 <li> 태그에 v-on:click 속성으로 클릭 시 checkTodo를 호출하도록 하고, v-for 구문에서 얻은 index를 파라미터로 보내도록 하였습니다.
클릭을 하면 checkTodo가 호출되면서 내부에서 파라미터로 넘어온 index값을 받아 리스트 중 해당 인덱스의 endDate 값을 설정해줍니다. 이미 체크가 되어 endDate가 있는 경우에는 endDate를 공백으로 만들어서 체크가 해제되도록 하였습니다.
그리고 이 endDate값에 따라 클래스가 바뀌도록 합니다. <li> 태그에는 item에 endDate가 있을 경우, done이라는 클래스를 부여합니다. font awesome을 사용한 체크 아이콘 역할을 하는 <i> 태그에는 item에 endDate가 있을 경우 'fa-check-square'라는 체크가 된 박스 아이콘을, 아닌 경우에는 'fa-square'라는 빈 박스 아이콘을 나타내는 클래스를 부여합니다. 이렇게 하면 vue가 endDate 값이 업데이트되는 것을 감지해 태그가 반응하여 클래스를 toggle 하도록 해줍니다.
이렇게 2편의 포스트에 걸쳐서 간단한 예제로 vue 기능을 살펴보았습니다. 사실 간단한 예제여서 화면 개발에서 주로 쓰이는 필수 기능들만 다뤘는데, vue에는 이 외에도 component를 비롯한 강력한 기능들이 많습니다. vue에 관심이 있으시면, 가이드 문서가 잘 정리되어 있으니 쭉 읽어보시면서 예제 코드를 만들어보시면서 vue의 강력한 기능들을 경험해보시면 좋을 것 같습니다.
Vue.js 시리즈 1편에 대한 내용을 살펴보고 싶다면, 아래 링크를 클릭해주세요.
▶ Vue.js를 이용한 Todo리스트 예제 만들기(1)
'프론트엔드' 카테고리의 다른 글
일렉트론 개발환경 구축하기 (1) | 2019.05.03 |
---|---|
Electron 개념정리 (0) | 2019.04.11 |
Vue.js를 이용한 Todo리스트 예제 만들기(1) (1) | 2018.03.15 |
Three.js를 이용한 3D 그래픽 입문기 (0) | 2018.02.08 |
Node.js 기초부터 튼튼히 (3) 이벤트 (1) | 2018.01.08 |