ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Vue 데이터 바인딩 실습 - TODO 앱 구현하기
    개발/Front-end 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 브런치에 존재합니다.

     

     

     

    참고

    '개발 > Front-end' 카테고리의 다른 글

    Vue HTTP 통신 라이브러리 - axios  (0) 2021.09.03
    Vue 데이터 바인딩  (0) 2021.08.25
    Vue 컴포넌트 Basic  (0) 2021.08.24
    Vue Lazy Load(비동기 컴포넌트)란?  (0) 2021.08.24
    Vue Lazy load 적용하기  (0) 2021.08.24

    댓글

Designed by Tistory.