개발/Front-end

Vue 데이터 바인딩 실습 - TODO 앱 구현하기

정찡이 2021. 9. 15. 22:28
728x90

이번 시간에는 이를 이용할 TODO 앱을 만들어보겠습니다.

0. 프로젝트 소개 및 컴포넌트 설계

0-1. 프로젝트 소개

이번에 실습할 프로젝트는 아래와 같습니다. (gif라서 약간 이미지가 깨집니다...)

0-2. 컴포넌트 구성

컴포넌트를 작게하는 경우 재사용성이 높아집니다. 따라서 아래와 같은 컴포넌트로 구성하겠습니다.

  • TodoHeader: 제목
  • TodoInput: 할일 목록 추가
    • 할일 목록 타이핑하여 + 버튼 클릭 시, localStorage에 추가(할 일 목록에 추가)
    • 할 일 목록 입력 후 엔터 클릭 시 할 일 목록에 추가
  • TodoList: 할일 리스트
    • localStorage에서 할 일 목록 데이터를 가져와서 보여주기
    • 완료 유무 체크, 리스트 삭제
  • TodoFooter: 모든 리스트를 없애는 버튼

 

 

0-3. 프로젝트 구현 순서

  1. Vue CLI 프로젝트 생성하기
  2. 컴포넌트 생성
  3. 파비콘, 아이콘, 폰트, 반응형 태그 설정
  4. 각 컴포넌트 구현
  5. 그 외 리팩토링은 추후에 추가 예정

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. 파비콘, 아이콘, 폰트, 반응형 태그 설정

3-1. 반응형 웹(뷰포트) 추가

레이아웃 크기에 따라 깨지지 않기 위해서 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 코드 설명

  1. v-model를 통해 데이터 바인딩을 하였습니다.
  2. v-on:keyup.enter로 엔더 입력 시 addTodo메서드(입력된 todo 목록이 추가) 실행이 가능하도록 하였습니다.
  3. v-on:click으로 해당 버튼 클릭 시 addTodo메소드가 실행됩니다.
  4. 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 코드 설명

  1. newTodoItem은 데이터 바인딩 시 사용됩니다.
  2. addTodo 메소드: newTodoItem데이터가 비지 않은 경우 localStorage에 입력한 데이터를 넣어줍니다.
    • {completed:false, item: this.newTodoItem}: completed는 할 일 완료 유무, item은 할일
    • setItem를 통해 localStorage에 넣어줍니다. 키는 newTodoItem, 값은 JSON.stringify(obj)로 넣어줍니다.
    • 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 코드 설명

  1. v-for를 이용하여 todoItems(할 일 리스트)를 하나씩 꺼냅니다.
  2. 체크 버튼 클릭 시 완료 유무를 체크하게 되는 코드입니다.
  • 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>
  1. 휴지통 버튼 클릭 시 해당할 일 삭제
  • 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 코드 설명 

  1. removeTodo 메소드: 해당 todoItem를 localStorage에서 제거하고 todoItems 데이터에서 해당 내용을 지워줍니다. vue 수업에서는 이런식으로 지우고 새로 만드는 것으로 알려주셨지만 굳이 제거할 필요없이 setItem 메소드만으로 업데이트가 가능합니다. 
  2. toggleComplete 메소드: 업데이트를 하기 위해 기존 todoItem를 제거하고 다시 새로 localStorage에 넣어줍니다.
  3. 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 브런치에 존재합니다.

 

 

 

참고

반응형