Vue 데이터 바인딩 실습 - TODO 앱 구현하기
이번 시간에는 이를 이용할 TODO 앱을 만들어보겠습니다.
0. 프로젝트 소개 및 컴포넌트 설계
0-1. 프로젝트 소개
이번에 실습할 프로젝트는 아래와 같습니다. (gif라서 약간 이미지가 깨집니다...)

0-2. 컴포넌트 구성
컴포넌트를 작게하는 경우 재사용성이 높아집니다. 따라서 아래와 같은 컴포넌트로 구성하겠습니다.
- TodoHeader: 제목
- TodoInput: 할일 목록 추가
- 할일 목록 타이핑하여 + 버튼 클릭 시, localStorage에 추가(할 일 목록에 추가)
- 할 일 목록 입력 후 엔터 클릭 시 할 일 목록에 추가
- TodoList: 할일 리스트
- localStorage에서 할 일 목록 데이터를 가져와서 보여주기
- 완료 유무 체크, 리스트 삭제
- TodoFooter: 모든 리스트를 없애는 버튼
0-3. 프로젝트 구현 순서
- Vue CLI 프로젝트 생성하기
- 컴포넌트 생성
- 파비콘, 아이콘, 폰트, 반응형 태그 설정
- 파비콘 생성: https://www.favicon-generator.org/
- 반응형 웹(뷰포트): https://www.w3schools.com/css/css_rwd_viewport.asp
- 아이콘(fontawesome): https://fontawesome.com/
- 구글 폰트(ubuntu): https://fonts.google.com/specimen/Ubuntu
- 각 컴포넌트 구현
- 그 외 리팩토링은 추후에 추가 예정
1. 뷰 CLI로 프로젝트 생성하기
$ npm install -g @vue/cli
$ vue create vue-todo
- default(vue3) 선택
$ cd vue-todo
$ npm run serve
2. 컴포넌트 생성 및 등록하기
2-1 컴포넌트 생성
vue-todo/src/components 폴더에 아래와 같이 총 4개의 컴포넌트를 추가합니다.

각 컴포넌트 파일에 template태그 안에 컴포넌트 이름을 넣어줍니다. 예를 들면 TodoHeader.vue 파일은 아래와 같이 입력합니다.
<template>
<div>
TodoHeader
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
2-2. App.vue에 등록하기
App.vue은 상위 컴포넌트입니다. 따라서 지금까지 만든 컴포넌트들을 vue-todo/src/App.vue에 아래와 같이 컴포넌트를 임포트하고 template에 해당 컴포넌트 태그를 넣어줍니다.
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput></TodoInput>
<TodoList></TodoList>
<TodoFooter></TodoFooter>
</div>
</template>
<script>
import TodoHeader from './components/TodoHeader.vue'
import TodoInput from './components/TodoInput.vue'
import TodoList from './components/TodoList.vue'
import TodoFooter from './components/TodoFooter.vue'
export default {
components: {
'TodoHeader': TodoHeader,
'TodoInput': TodoInput,
'TodoList': TodoList,
'TodoFooter': TodoFooter
}
}
</script>
<style>
body {
text-align: center;
background-color: #F6F6F6;
}
input {
border-style: groove;
width: 200px;
}
button {
border-style: groove;
}
.shadow {
box-shadow: 5px 10px 10px rgba(0, 0, 0, 0.03);
}
</style>
결과를 보면 아래와 같이 4개의 컴포넌트가 제대로 생성된 것을 확인할 수 있습니다.

3. 파비콘, 아이콘, 폰트, 반응형 태그 설정
- 파비콘 생성: https://www.favicon-generator.org/
- 반응형 웹(뷰포트): https://www.w3schools.com/css/css_rwd_viewport.asp
- 아이콘(fontawesome): https://fontawesome.com/
- 구글 폰트(ubuntu): https://fonts.google.com/specimen/Ubuntu
3-1. 반응형 웹(뷰포트) 추가
레이아웃 크기에 따라 깨지지 않기 위해서 vue-todo/public/index.html에 아래 코드를 추가합니다. 이미 되어있는 경우 넘어갑니다.
- 참고 사이트: https://www.w3schools.com/css/css_rwd_viewport.asp
- 경로: vue-todo/public/index.html
<meta name="viewport" content="width=device-width, initial-scale=1.0">
3-2. 파비콘
https://www.favicon-generator.org/ 사이트에서 생성하여 vue-todo/public/index.html에 파비콘 경로를 넣어줍니다. 이미 되어있는 경우 넘어갑니다.
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
3-3. 아이콘 & 폰트
https://fonts.google.com/ 에서 아이콘과 폰트 모두 사용하도록 합니다. vue-todo/public/index.html에 아래 코드를 추가합니다.
<link href="https://fonts.googleapis.com/icon?family=Material+Icons"
rel="stylesheet">
3-3-1. 폰트 바꾸는 법
https://fonts.google.com/ 접속하여 원하는 폰트 선택 후, 아래와 같이 선택한 후에 아래 소스를 복사하여 원하는 컴포넌트 폴더 style에 추가해줍니다.

예를 들면 저는 전체 컴포넌트에 적용하기 위해서 vue-todo/src/App.vue에서 추가해 줬습니다.
<style>
@import url('https://fonts.googleapis.com/css2?family=Caveat&display=swap');
</style>
글씨체 적용된 모습은 아래와 같습니다.

3-4. 전체 index.html 코드
파비콘, 아이콘, 폰트, 반응형 태그 설정 모두 적용된 index.html 소스입니다.
- 경로: vue-todo/public/index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
4. TodoHeader 구현
아래와 같이 제목을 추가 후 스타일을 넣어줍니다.
- 경로: vue-todo/src/components/TodoHeader.vue
<template>
<header>
<h1>Todo it!</h1>
</header>
</template>
<style scoped>
h1 {
color: #2F3B52;
font-weight: 900;
margin: 2.5rem 0 1.5rem;
}
</style>
적용된 모습은 아래와 같습니다.

5. TodoInput
- 경로: vue-todo/src/components/TodoInput.vue
- 기능
- 할일 목록 타이핑하여 + 버튼 클릭 시, localStorage에 추가(할 일 목록에 추가)
- 할 일 목록 입력 후 엔터 클릭 시 할 일 목록에 추가
5-1. template 코드 설명
- v-model를 통해 데이터 바인딩을 하였습니다.
- v-on:keyup.enter로 엔더 입력 시 addTodo메서드(입력된 todo 목록이 추가) 실행이 가능하도록 하였습니다.
- v-on:click으로 해당 버튼 클릭 시 addTodo메소드가 실행됩니다.
- add으로 + 아이콘을 추가하였습니다.
<template>
<div class="inputBox shadow">
<input type="text" v-model="newTodoItem" v-on:keyup.enter="addTodo">
<span class="addContainer" v-on:click="addTodo">
<i class="material-icons addBtn">add</i>
</span>
</div>
</template>
5-2. script 코드 설명
- newTodoItem은 데이터 바인딩 시 사용됩니다.
- addTodo 메소드: newTodoItem데이터가 비지 않은 경우 localStorage에 입력한 데이터를 넣어줍니다.
- {completed:false, item: this.newTodoItem}: completed는 할 일 완료 유무, item은 할일
- setItem를 통해 localStorage에 넣어줍니다. 키는 newTodoItem, 값은 JSON.stringify(obj)로 넣어줍니다.
- localStorage 설명: https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
- this.clearInput()를 실행하여 input에 있는 데이터를 빈값으로 바꿔줍니다.
<script>
export default {
data() {
return {
newTodoItem: ""
}
},
methods: {
addTodo: function (){
if (this.newTodoItem != ''){
var obj = {completed:false, item: this.newTodoItem};
console.log(this.newTodoItem);
// 저장하는 로직
localStorage.setItem(this.newTodoItem, JSON.stringify(obj)); // 스트링으로 변경되도록함
this.clearInput();
}
},
clearInput: function (){
this.newTodoItem = '';
}
}
}
</script>
5-3. TodoInput.vue 전체 소스
<template>
<div class="inputBox shadow">
<input type="text" v-model="newTodoItem" v-on:keyup.enter="addTodo">
<span class="addContainer" v-on:click="addTodo">
<i class="material-icons addBtn">add</i>
</span>
</div>
</template>
<script>
export default {
data() {
return {
newTodoItem: ""
}
},
methods: {
addTodo: function (){
if (this.newTodoItem != ''){
var obj = {completed:false, item: this.newTodoItem};
console.log(this.newTodoItem);
// 저장하는 로직
localStorage.setItem(this.newTodoItem, JSON.stringify(obj)); // 스트링으로 변경되도록함
this.clearInput();
}
},
clearInput: function (){
this.newTodoItem = '';
}
}
}
</script>
<style scoped>
input:focus {
outline: none;
}
.inputBox {
background: white;
height: 50px;
line-height: 50px;
border-radius: 5px;
}
.inputBox input {
border-style: none;
font-size: 0.9rem;
}
.addContainer {
float: right;
background: linear-gradient(to right, #62EAC6, #32CEE6);
display: block;
width: 3rem;
border-radius: 0 5px 5px 0;
}
.addBtn {
color: white;
vertical-align: middle;
}
</style>
6. TodoList
- 경로: vue-todo/src/components/TodoList.vue
- 기능
- localStorage에서 할일 목록 데이터를 가져와서 보여주기
- 완료 유무 체크, 리스트 삭제
6-1. template 코드 설명
- v-for를 이용하여 todoItems(할 일 리스트)를 하나씩 꺼냅니다.
- 체크 버튼 클릭 시 완료 유무를 체크하게 되는 코드입니다.
- v-bind:class를 이용하여 todoItem.completed가 true인 경우, checkBtnCompleted 클래스를 추가하여 해당 클래스 스타일을 넣게 됩니다.(회색으로 변경& 글씨에 줄 넣기)
- v-on:click="toggleComplete(todoItem, index): 체크 버튼 클릭 시 todoItem.completed가 토글 되도록 합니다.
<span class="checkBtn material-icons" v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleComplete(todoItem, index)">done</span>
- 휴지통 버튼 클릭 시 해당할 일 삭제
- removeTodo(todoItem, index)를 통해 해당 목록이 삭제가 됩니다.
<span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
<i class="material-icons">delete</i>
</span>
<template>
<div>
<ul>
<li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem" class="shadow">
<span class="checkBtn material-icons" v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleComplete(todoItem, index)">done</span>
<span v-bind:class="{textCompleted: todoItem.completed}">{{ todoItem.item }}</span>
<span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
<i class="material-icons">delete</i>
</span>
</li>
</ul>
</div>
</template>
6-2. script 코드 설명
- removeTodo 메소드: 해당 todoItem를 localStorage에서 제거하고 todoItems 데이터에서 해당 내용을 지워줍니다. vue 수업에서는 이런식으로 지우고 새로 만드는 것으로 알려주셨지만 굳이 제거할 필요없이 setItem 메소드만으로 업데이트가 가능합니다.
- toggleComplete 메소드: 업데이트를 하기 위해 기존 todoItem를 제거하고 다시 새로 localStorage에 넣어줍니다.
- created: 인스턴스 생성 후 호출되는 곳으로 데이터 초기 선언 시 넣어줍니다. 여기서도 localStorage에 있는 데이터를 불러와서 todoItems data 리스트에 넣어줍니다. JSON.parse를 하는 이유는 string 변환한 object를 다시 object로 변환하기 위해서 사용합니다.
<script>
export default {
data() {
return {
todoItems: []
}
},
methods: {
removeTodo: function(todoItem, index){
console.log(todoItem, index);
localStorage.removeItem(todoItem);
this.todoItems.splice(index, 1); // 특정 인덱스를 지우게된다.
},
toggleComplete: function(todoItem, index) {
console.log(todoItem, index);
todoItem.completed = !todoItem.completed;
// 업데이트 - 해당 기능이 없어 삭제 후 삽입
localStorage.setItem(todoItem.item, JSON.stringify(todoItem));
}
},
created: function() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i ++) {
if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
console.log(JSON.parse(localStorage.getItem(localStorage.key(i))));
this.todoItems.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
}
}
}
</script>
6-3. TodoList.vue 전체 소스
<template>
<div>
<ul>
<li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem" class="shadow">
<span class="checkBtn material-icons" v-bind:class="{checkBtnCompleted: todoItem.completed}"
v-on:click="toggleComplete(todoItem, index)">done</span>
<span v-bind:class="{textCompleted: todoItem.completed}">{{ todoItem.item }}</span>
<span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
<i class="material-icons">delete</i>
</span>
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
todoItems: []
}
},
methods: {
removeTodo: function(todoItem, index){
console.log(todoItem, index);
localStorage.removeItem(todoItem);
this.todoItems.splice(index, 1); // 특정 인덱스를 지우게된다.
},
toggleComplete: function(todoItem, index) {
console.log(todoItem, index);
todoItem.completed = !todoItem.completed;
// 업데이트 - 해당 기능이 없어 삭제 후 삽입
localStorage.setItem(todoItem.item, JSON.stringify(todoItem));
}
},
created: function() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i ++) {
if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
console.log(JSON.parse(localStorage.getItem(localStorage.key(i))));
this.todoItems.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
}
}
}
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 0;
margin-top: 0;
text-align: left;
}
li {
display: flex;
min-height: 50px;
height: 50px;
line-height: 50px;
margin: 0.5rem 0;
padding: 0 0.9rem;
background: white;
border-radius: 5px;
}
.checkBtn {
line-height: 45px;
color: #62acde;
margin-right: 5px;
}
.checkBtnCompleted {
color: #b3adad;
}
.textCompleted {
text-decoration: line-through;
color: #b3adad;
}
.removeBtn {
margin-left: auto;
color: #de4343;
}
</style>
7. TodoFooter
- 경로: vue-todo/src/components/TodoFooter.vue
clearTodo를 클릭하면 localStorage에 내용은 모두 제거합니다.
<template>
<div class="clearAllContainer">
<span class="clearAllBtn" v-on:click="clearTodo">Clear All
</span>
</div>
</template>
<script>
export default {
methods: {
clearTodo: function() {
localStorage.clear();
}
}
}
</script>
<style scoped>
.clearAllContainer {
width: 8.5rem;
height: 50px;
line-height: 50px;
background-color: white;
border-radius: 5px;
margin: 0 auto;
}
.clearAllBtn {
color: #e20303;
display: block;
}
</style>
다음 시간에는 해당 소스를 리팩토링을 진행하겠습니다.
해당 내용 전체 소스는 아래 링크에서 확인할 수 있습니다.
https://github.com/JUNGEEYOU/VueTodo.git에서 todo_implement 브런치에 존재합니다.