2 Todo App - 프로젝트 구현

source: categories/study/vue-beginner-lv2/vue-beginner-lv2_2.md

2.1 컴포넌트 생성 및 등록하기


vue-todo/
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|       `-- logo.png
|   |-- components/
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- babel.config.js
|-- package.json
|-- README.md
`-- yarn.lock

src/components 폴더 안에 사용할 컴포넌트들을 등록하면됩니다.


vue-todo/
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|       `-- logo.png
|   |-- components/
|       `-- TodoHeader.vue
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- babel.config.js
|-- package.json
|-- README.md
`-- yarn.lock

TodoHeader.vue



<template>
    <div>header</div>
</template>

<script>
export default {
  name: "TodoHeader"
}
</script>

<style scoped>

</style>


TodoInput.vue


vue-todo/
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|       `-- logo.png
|   |-- components/
|       |-- TodoHeader.vue
|       `-- TodoInput.vue
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- babel.config.js
|-- package.json
|-- README.md
`-- yarn.lock



<template>
  <div>input</div>
</template>

<script>
export default {
  name: "TodoInput"
}
</script>

<style scoped>

</style>


TodoList.vue


vue-todo/
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|       `-- logo.png
|   |-- components/
|       |-- TodoHeader.vue
|       |-- TodoInput.vue
|       `-- TodoList.vue
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- babel.config.js
|-- package.json
|-- README.md
`-- yarn.lock



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

<script>
export default {
  name: "TodoList"
}
</script>

<style scoped>

</style>


TodoFooter.vue


vue-todo/
|-- public/
|   |-- favicon.ico
|   `-- index.html
|-- src/
|   |-- assets/
|       `-- logo.png
|   |-- components/
|       |-- TodoFooter.vue
|       |-- TodoHeader.vue
|       |-- TodoInput.vue
|       `-- TodoList.vue
|   |-- App.vue
|   `-- main.js
|-- .gitignore
|-- babel.config.js
|-- package.json
|-- README.md
`-- yarn.lock



<template>
    <div>footer</div>
</template>

<script>
export default {
  name: "TodoFooter"
}
</script>

<style scoped>

</style>


src/App.vue



<template>
  <div id="app">
    <TodoHeader></TodoHeader>
    <todo-header></todo-header>
    <TodoInput></TodoInput>
    <TodoList></TodoList>
    <TodoFooter></TodoFooter>
  </div>
</template>

<script>
import TodoFooter from "@/components/TodoFooter";
import TodoHeader from "@/components/TodoHeader";
import TodoInput from "@/components/TodoInput";
import TodoList from "@/components/TodoList";

// 원래 vue 컴포넌트 등록 방법 - 이렇게 싱글 파일 컴포넌트가아닌 cdn으로 불러와서 작업할 때
// var my_cmp = {
//   template: '<div>my component</div>'
// }
//
// new Vue({
//   el: '',
//   components: {
//     // '컴포넌트 이름': '컴포넌트 내용'
//     // 컨벤션상 컴포넌트 이름 가운데엔 하이푼이 들어가는 것이 좋다. 케밥 문법.
//     'my-cmp': my_cmp
//   }
// })

// 위와 같은 관점에서 아래도 동일하게 등록하면 된다.

export default {
  components: {
    TodoHeader, // 이번엔 파스칼 케이스로 작성했다. 각 단어 맨 앞글자가 대문자.
                // 그런데 사실 스크립트에서 사용하는 컴포넌트 이름은 파스칼 케이스로 작성하고
                // 실제 html 태그에 넣을 때는 케밥 기법으로 넣으라고 가이드가 되어있다.
    TodoInput,
    TodoList,
    TodoFooter
  }
}
</script>

<style>

</style>



npm run serve
# or
yarn serve

2.2 파비콘, 아이콘, 폰트, 반응형 태그 설정하기

2.2.1 반응형 태그 설정하기 - public/index.html

<meta name="viewport" content="width=device-width,initial-scale=1.0">

public/index.html 파일에 위 태그 넣어주면 된다는 얘긴데, @vue/cli 2점대 버전엔 위 태그가 안들어가있었나보네..
지금은 public/index.html 파일에 위 태그 기본으로 들어가있다.

2.2.2 favicon

위 사이트에서 src/assets/logo.png 이미지를 favicon.ico 파일로 만듭니다.

생성하고나서 위 경로로 이미지를 넣어줍니다.
그런데 파비콘 관련 태그도 이미 들어가있는 듯 합니다. 최신 버전엔.

<link rel="icon" href="<%= BASE_URL %>favicon.ico">

위 태그가 이미 public/index.html 파일에 있습니다.

2.2.3 Font Awesome



<!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 rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
    <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>


2.2.4 google font

link 태그를 그대로 복사하시면 됩니다.



<!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 rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
    <link href="https://fonts.googleapis.com/css2?family=Ubuntu:ital,wght@0,300;0,400;0,500;1,300;1,400&display=swap" 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>


2.3 TodoHeader 컴포넌트 구현



<template>
  <header>
    <h1>TODO it!</h1>
  </header>
</template>

<script>
export default {
  name: "TodoHeader"
}
</script>

<!-- vue single file component에서만 지원하는 속성 scoped -->
<!-- scoped를 아래와 같이 명시하면 아래 style 태그 안에 작성되는 css는 이 컴포넌트에만 유효하도록 적용이됩니다. -->
<style scoped>
h1 {
  color: #2f3b52;
  font-weight: 900;
  margin: 2.5rem 0 1.5rem;
}
</style>


위와 같이 TodoHeader 컴포넌트가 꾸며진걸 볼 수 있습니다.

2.3.1 src/App.vue



<template>
  <div id="app">
    <TodoHeader></TodoHeader>
    <todo-header></todo-header>
    <TodoInput></TodoInput>
    <TodoList></TodoList>
    <TodoFooter></TodoFooter>
  </div>
</template>

<script>
import TodoFooter from "@/components/TodoFooter";
import TodoHeader from "@/components/TodoHeader";
import TodoInput from "@/components/TodoInput";
import TodoList from "@/components/TodoList";

// 원래 vue 컴포넌트 등록 방법 - 이렇게 싱글 파일 컴포넌트가아닌 cdn으로 불러와서 작업할 때
// var my_cmp = {
//   template: '<div>my component</div>'
// }
//
// new Vue({
//   el: '',
//   components: {
//     // '컴포넌트 이름': '컴포넌트 내용'
//     // 컨벤션상 컴포넌트 이름 가운데엔 하이푼이 들어가는 것이 좋다. 케밥 문법.
//     'my-cmp': my_cmp
//   }
// })

// 위와 같은 관점에서 아래도 동일하게 등록하면 된다.

export default {
  components: {
    TodoHeader, // 이번엔 파스칼 케이스로 작성했다. 각 단어 맨 앞글자가 대문자.
                // 그런데 사실 스크립트에서 사용하는 컴포넌트 이름은 파스칼 케이스로 작성하고
                // 실제 html 태그에 넣을 때는 케밥 기법으로 넣으라고 가이드가 되어있다.
    TodoInput,
    TodoList,
    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>


2.4 TodoInput 컴포넌트의 할 일 저장 기능 구현

TodoInput 컴포넌트의 역할 : 인풋 박스를 하나 만들어서 특정 텍스트 값을 입력을하면 크 텍스트 값을 로컬 스토리지에 저장을 합니다.



<template>
  <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
  <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
  <input type="text" v-model="newTodoItem">
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  }
}
</script>

<style scoped>

</style>


입력을 한글자씩 할 때마다 datanewTodoItem이 실시간으로 변하는 것을 확인할 수 있습니다.
angularreact의 장점을 결합한게 vue.js라고 말씀드렸었는데, angular의 장점이 그대로 반영이 되었습니다.
반대로 크롬개발자창 - 즉, 스크립트단에서 datanewTodoItem 값을 수정해도 그 값이 화면에 바로 반영이됩니다.

즉, 화면에서의 DOM 조작, 데이터 변경과 Vue 안에서 인스턴스의 데이터의 변경이 서로 동일하게 동기화가되는 것을 보실 수가 있습니다.



<template>
  <div>
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <input type="text" v-model="newTodoItem">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <button v-on:click="addTodo">add</button>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      console.log(this.newTodoItem);
    }
  }
}
</script>

<style scoped>

</style>


Note

this는 결국 main.js에서 생성자 함수 new Vue()로 생성한 하나의 Vue 인스턴스를 가리킨다.

Note

options 속성이나 콜백에 created: () =&gt; console.log(this.a) 이나 vm.$watch('a', newValue =&gt; this.myMethod()) 와 같은 화살표 함수 사용을 지양하기 바랍니다.
화살표 함수는 this를 가지지 않기 때문에 화살표 함수에서의 this는 다른 변수로 취급되거나 렉시컬하게 호출한 변수를 발견할 때까지 부모 스코프에서 해당 변수를 찾습니다.
이 때문에 Uncaught TypeError: Cannot read property of undefined 또는 Uncaught TypeError: this.myMethod is not a function와 같은 오류가 발생하게 됩니다.

2.4.1 input 입력 후 add 버튼 누르면 input 창 값 삭제

입력 다 하고 add 버튼을 눌렀을 때 인풋창의 텍스트를 삭제하도록 기능을 넣었습니다.



<template>
  <div>
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <input type="text" v-model="newTodoItem">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <button v-on:click="addTodo">add</button>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      console.log(this.newTodoItem);
      // 저장하는 로직
      // localStorage.setItem();
      // 이런식으로 로컬 스토리지에 저장하고 지우는 로직으로 작성하면됩니다.
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>

</style>


2.4.2 input text를 localStorage 저장하기



<template>
  <div>
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <input type="text" v-model="newTodoItem">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <button v-on:click="addTodo">add</button>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      // 저장하는 로직
      // 로컬스토리지 저장하는 방법: setItem(키, 값)이라는 API가 있습니다.
      // 일단 간단하게 저장하는 것이기 때문에 키랑 값을 똑같게하겠습니다.
      localStorage.setItem(this.newTodoItem, this.newTodoItem);
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>

</style>


2.5 TodoInput 컴포넌트 코드 정리 및 UI 스타일링

2.5.1 input 값 삭제 기능 분리



<template>
  <div>
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <!-- v-model은 2-way binding이라고 말씀드렸죠? 화면 데이터와 스크립트단 데이터가 동기화가 된다. -->
    <input type="text" v-model="newTodoItem">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <button v-on:click="addTodo">add</button>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      // 저장하는 로직
      // 로컬스토리지 저장하는 방법: setItem(키, 값)이라는 API가 있습니다.
      // 일단 간단하게 저장하는 것이기 때문에 키랑 값을 똑같게하겠습니다.
      localStorage.setItem(this.newTodoItem, this.newTodoItem);
      this.clearInput();
    },
    clearInput: function () {
      // addTodo 함수에선 저장하는 역할만하고 clearInput 함수에서 input 값을 지워주는 역할을 한다.
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>

</style>


Note

위와 같이하면 input 값 지우는 것과 로컬스토리지에 저장하는 함수를 분리할 수 있습니다.

2.5.2 스타일링



<template>
  <div class="inputBox shadow">
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <!-- v-model은 2-way binding이라고 말씀드렸죠? 화면 데이터와 스크립트단 데이터가 동기화가 된다. -->
    <input type="text" v-model="newTodoItem">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <!-- <button v-on:click="addTodo">add</button> -->
    <span class="addContainer" v-on:click="addTodo">
      <i class="fas fa-plus addBtn"></i>
    </span>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      // 저장하는 로직
      // 로컬스토리지 저장하는 방법: setItem(키, 값)이라는 API가 있습니다.
      // 일단 간단하게 저장하는 것이기 때문에 키랑 값을 똑같게하겠습니다.
      localStorage.setItem(this.newTodoItem, this.newTodoItem);
      this.clearInput();
    },
    clearInput: function () {
      // addTodo 함수에선 저장하는 역할만하고 clearInput 함수에서 input 값을 지워주는 역할을 한다.
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>
.input:focus {
  outline: none;
}
.inputBox {
  background-color: #fff;
  height: 50px;
  line-height: 50px;
  border-radius: 5px;
}
.inputBox input {
  border-style: none;
  font-size: 0.9rem;
}
.addContainer {
  float: right;
  display: block;
  background: linear-gradient(to right, #6478fb, #8763fb);
  width: 3rem;
  border-radius: 0 5px 5px 0;
}
.addBtn {
  color: #ffffff;
  vertical-align: middle;
}
</style>


2.5.3 enter 키 동작



<template>
  <div class="inputBox shadow">
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <!-- v-model은 2-way binding이라고 말씀드렸죠? 화면 데이터와 스크립트단 데이터가 동기화가 된다. -->
    <!-- keyup.enter: enter키가 눌린 후 키업했을 때 실행  -->
    <input type="text" v-model="newTodoItem" v-on:keyup.enter="addTodo">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <!-- <button v-on:click="addTodo">add</button> -->
    <span class="addContainer" v-on:click="addTodo">
      <i class="fas fa-plus addBtn"></i>
    </span>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      // 저장하는 로직
      // 로컬스토리지 저장하는 방법: setItem(키, 값)이라는 API가 있습니다.
      // 일단 간단하게 저장하는 것이기 때문에 키랑 값을 똑같게하겠습니다.
      localStorage.setItem(this.newTodoItem, this.newTodoItem);
      this.clearInput();
    },
    clearInput: function () {
      // addTodo 함수에선 저장하는 역할만하고 clearInput 함수에서 input 값을 지워주는 역할을 한다.
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>
.input:focus {
  outline: none;
}
.inputBox {
  background-color: #fff;
  height: 50px;
  line-height: 50px;
  border-radius: 5px;
}
.inputBox input {
  border-style: none;
  font-size: 0.9rem;
}
.addContainer {
  float: right;
  display: block;
  background: linear-gradient(to right, #6478fb, #8763fb);
  width: 3rem;
  border-radius: 0 5px 5px 0;
}
.addBtn {
  color: #ffffff;
  vertical-align: middle;
}
</style>


2.6 TodoList 컴포넌트의 할 일 목록 표시 기능 구현

2.6.1 vue life cycle

  1. vue instance 생성 - created
  2. mounted - 탑재, DOM에 부착
  3. update
  4. destroy

위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.



<template>
  <div>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    console.log('created');
  },
}
</script>

<style scoped>

</style>


위와 같이 새로고침을 하시면 콘솔창에 created 텍스트가 찍히는 것을 보실 수 있습니다.
인스턴스가 생성되자마자 created 로직이 호출되는 것입니다.

2.6.2 로컬스토리지 데이터를 vue 인스턴스의 data로 옮기기 1

로컬스토리지 데이터를 vue 인스턴스의 data로 옮기는 코드를 작성하다가 궁금한점이있어 알아본 것이 있습니다.



<template>
  <div>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      console.log(Object.prototype.hasOwnProperty.call(localStorage, 'hi'))

      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // this.todoItems.push(localStorage.key(i));
        console.log(localStorage.key(i)); // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
      }
    }
  },
}
</script>

<style scoped>

</style>


localStorage 객체에서 hasOwnProperty 메소드를 사용하려면 위와 같이 Object.prorotype.hasOwnProperty.call() 형태로 사용해야됩니다.
이는 document.querySelectAll()과 같은 걸로 선택했을 때 Array.prorotype.slice.call()을 통해 유사배열을 배열로 만들어주는 것과 같습니다.

document.querySelectAll()로 선택하면
NodeList라는 데이터형태로 값이 반환이되는데,
이는 유사배열 형태로 완전한 배열 형태가 아닙니다.
따라서 보통은 배열.slice()로 쓸 수 있는 것을 유사배열은 Array.prototype.slice.call(유사배열, 인자..) 형태로 쓰게되는 것입니다.

마찬가지로 localStorage 객체도 유사배열 객체(Array-like objects)입니다.

2.6.2.1 공부 중 궁금증: 유사배열 객체 (Array-like objects)

var array = [1, 2, 3];
array; // [1, 2, 3]
var nodes = document.querySelectorAll('div'); // NodeList [div, div, div, div, div, ...]
var els = document.body.children; // HTMLCollection [noscript, link, div, script, ...]

nodes와 els는 프론트엔드 개발을 하다보면 많이 접하는 친구들입니다.
위 예제에서 array는 배열이고, nodes와 els는 유사배열입니다.
둘의 차이를 아시겠나요?
겉만 봐서는 잘 모릅니다.
둘 다 비슷하게 []로 감싸져있기 때문입니다.
Array.isArray 메서드(배열인지를 판단해주는 메소드)를 사용해서 뭐가 배열인지 확인해보겠습니다.

Array.isArray(array); // true
Array.isArray(nodes); // false
Array.isArray(els); // false

직접 배열 리터럴로 선언한 array만 배열입니다.
비슷한 방법으로 array instanceof Array로도 판단할 수 있습니다.

nodes나 els처럼 []로 감싸져있지만 배열이 아닌 친구들을 유사배열이라고 부릅니다.
어떻게 이런 친구들이 만들어지는지 알아봅시다.

var yoosa = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3
};

yoosa 객체가 바로 유사배열입니다.
localStorage 객체와 똑같은 형태입니다~!
키가 숫자고, length라는 속성을 가지고 있습니다.
localStorage 객체는 키가 숫자형태는 아닌데.. 흐음..

여튼 배열도 객체라는 성질을 이용한 트릭입니다.
배열처럼 yoosa[0], yoosa[1], yoosa.length 같은 것을 모두 활용할 수 있습니다.
배열과 유사배열을 구분해야하는 이유는, 유사배열의 경우 배열의 메소드를 쓸 수 없기 때문입니다.

array.forEach(function(el) { console.log(el); }); // 1, 2, 3
els.forEach(function(el) { console.log(el); }); // Uncaught TypeError: els.forEach is not a function

els에 forEach 같은 배열 메서드를 사용하면 에러가 발생합니다.
nodes는 프로토타입에 forEach가 있어서 되긴합니다.
여튼 els는 배열이 아니므로 위와 같이 에러가 나는 것입니다. localStorage도 els처럼 그래서 사용 못함.

이럴 때 메소드를 빌려쓰는 방법이 있습니다.
배열 프로토타입에서 forEach 메소드를 빌려오는 것입니다.
바로 call이나 apply입니다.

Array.prototype.forEach.call(nodes, function(el) { console.log(el); });
[].forEach.call(els, function(el) { console.log(el); });

이제 유사배열에도 forEach를 사용할 수 있습니다.
map이나 filter, reduce 등의 다른 배열 메서드도 사용 가능합니다.

최신 자바스크립트에선 Array.from으로 더 간단하게 할 수 있습니다.

Array.from(nodes).forEach(function(el) { console.log(el) });

자주 보는(ES6에서는 더 이상 안보이지만) 유사배열이 하나 더 있습니다.
functionarguments입니다.
함수 선언문에 넣은 인자 목록을 표시하죠.

function arrayLike() {
  console.log(arguments);
}
arrayLike(4, 5, 6); // Arguments [4, 5, 6, callee, Symbol]

역시 forEach 같은 배열 메서드를 쓸 수 없으므로 문제가됩니다.
위에서 설명한 방법을 적용해야합니다.

function arrayLike() {
  console.log(arguments);
  [].forEach.call(arguments, function(el) { console.log(el) });
}
arrayLike(4, 5, 6);

유사배열. 별거아니죠?
[]로 감싸져있다고 다 같은 배열이 아니라는 것과, Array.isArray로 판별하는 방법, 배열 프로토타입에서 메소드를 빌려쓰는 방법에 대해 알아두시면 좋습니다!
유사배열과 자주 만나는 프론트엔드에선 필수입니다!

2.6.2 로컬스토리지 데이터를 vue 인스턴스의 data로 옮기기 2



<template>
  <div>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // this.todoItems.push(localStorage.key(i));
        console.log(localStorage.key(i)); // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
      }
    }
  },
}
</script>

<style scoped>

</style>


Note

loglevelwebpack-devserver로 프로토타이핑을 하고있기 때문에 자동으로 주입되는겁니다.
이건 지금 크게 신경을 안쓰셔도됩니다.
이따 프로토타이핑 기준으로 이 loglevel을 제외하는 구문도 넣어보겠습니다.



<template>
  <div>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
        this.todoItems.push(localStorage.key(i));
      }
    }
  },
}
</script>

<style scoped>

</style>


loglevel을 제외하는 구문을 넣어보도록 하겠습니다.



<template>
  <div>
    <ul>
      <li>1</li>
      <li>2</li>
      <li>3</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          this.todoItems.push(localStorage.key(i));
        }
      }
    }
  },
}
</script>

<style scoped>

</style>




<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <li v-for="todoItem in todoItems" v-bind:key="todoItem">{{todoItem}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          this.todoItems.push(localStorage.key(i));
        }
      }
    }
  },
}
</script>

<style scoped>

</style>


2.7 TodoList 컴포넌트 UI 스타일링



<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <li v-for="todoItem in todoItems" v-bind:key="todoItem" class="shadow">
        {{todoItem}}
        <span class="removeBtn" v-on:click="removeTodo">
          <i class="fas fa-trash-alt"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  methods: {
    removeTodo() {

    },
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          this.todoItems.push(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-color: #ffffff;
  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>


2.8 TodoList 컴포넌트 할 일 삭제 기능 구현



<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem" class="shadow">
        {{todoItem}}
        <span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
          <i class="fas fa-trash-alt"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  methods: {
    removeTodo(todoItem, index) {
      // 여기까지는 로컬스토리지, 쉽게말하면 DB의 영역이다.
      // 스크립트 영역이 아니기 때문에 이렇게 todoItem을 지운다고해서 이것이 바로 화면에 렌더링되진 않는다.
      // 렌더링되게하기 위해선 data의 todoItem 값도 수정해줘야한다.
      localStorage.removeItem(todoItem.item);
      // arr.splice(index, 1): 특정 index로부터 1개를 지운다는 뜻이다. 기존 배열 arr에 변형을 가한다.
      // 반면, slice는 arr.slice()를 해도 arr에 변화를 안가하고 새로운 배열을 반환한다.
      this.todoItems.splice(index, 1);
    },
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          this.todoItems.push(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-color: #ffffff;
  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>


2.9 TodoList 컴포넌트의 할 일 완료 기능 구현



<!-- src/components/TodoInput.vue -->
<template>
  <div class="inputBox shadow">
    <!-- v-model이 하는 역할: 인풋에 입력된 텍스트값을 동적으로 바로바로 vue 인스턴스 안에 매핑한다. -->
    <!-- input에 입력되는 값이 바로바로 vue 인스턴스 안에 특정 data에 매핑되도록 v-model 사용 -->
    <!-- v-model은 2-way binding이라고 말씀드렸죠? 화면 데이터와 스크립트단 데이터가 동기화가 된다. -->
    <!-- keyup.enter: enter키가 눌린 후 키업했을 때 실행  -->
    <input type="text" v-model="newTodoItem" v-on:keyup.enter="addTodo">
    <!-- 클릭했을 때 동작할 메소드를 설정 -->
    <!-- <button v-on:click="addTodo">add</button> -->
    <span class="addContainer" v-on:click="addTodo">
      <i class="fas fa-plus addBtn"></i>
    </span>
  </div>
</template>

<script>
export default {
  name: "TodoInput",
  data: function () {
    return {
      newTodoItem: ""
    }
  },
  methods: {
    // function 함수를 사용해야 Vue 인스턴스를 this가 가리킨다.
    // 화살표 함수를 사용하면 this가 Vue 인스턴스를 가리키지 못한다.
    addTodo: function () {
      // input에 입력된 값이 있을 때
      if (this.newTodoItem !== '') {
        var obj = {completed: false, item: this.newTodoItem};
        // 저장하는 로직
        // 로컬스토리지 저장하는 방법: setItem(키, 값)이라는 API가 있습니다.
        // 일단 간단하게 저장하는 것이기 때문에 키랑 값을 똑같게하겠습니다.
        // 자바스크립트 객체를 string으로 변환해주는 API, JSON.stringify()
        // JSON.stringify(obj)를 하지않고 obj로 넣는다면 localStorage에 [object Object] 형태로 값이 들어가게된다.
        // JSON.stringify(obj)를 하면 obj를 string으로 변환해서 넣는거기 때문에 {"completed": false, "item": "456"} 이런 형태로 저장되게된다.
        localStorage.setItem(this.newTodoItem, JSON.stringify(obj));

        this.clearInput();
      }
    },
    clearInput: function () {
      // addTodo 함수에선 저장하는 역할만하고 clearInput 함수에서 input 값을 지워주는 역할을 한다.
      this.newTodoItem = '';
    }
  }
}
</script>

<style scoped>
.input:focus {
  outline: none;
}
.inputBox {
  background-color: #fff;
  height: 50px;
  line-height: 50px;
  border-radius: 5px;
}
.inputBox input {
  border-style: none;
  font-size: 0.9rem;
}
.addContainer {
  float: right;
  display: block;
  background: linear-gradient(to right, #6478fb, #8763fb);
  width: 3rem;
  border-radius: 0 5px 5px 0;
}
.addBtn {
  color: #ffffff;
  vertical-align: middle;
}
</style>




<!-- src/components/TodoList.vue -->
<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem" class="shadow">
        <i class="checkBtn fas fa-check" v-on:click="toggleComplete"></i>
        {{todoItem}}
        <span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
          <i class="fas fa-trash-alt"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  methods: {
    removeTodo(todoItem, index) {
      // 여기까지는 로컬스토리지, 쉽게말하면 DB의 영역이다.
      // 스크립트 영역이 아니기 때문에 이렇게 todoItem을 지운다고해서 이것이 바로 화면에 렌더링되진 않는다.
      // 렌더링되게하기 위해선 data의 todoItem 값도 수정해줘야한다.
      localStorage.removeItem(todoItem.item);
      // arr.splice(index, 1): 특정 index로부터 1개를 지운다는 뜻이다. 기존 배열 arr에 변형을 가한다.
      // 반면, slice는 arr.slice()를 해도 arr에 변화를 안가하고 새로운 배열을 반환한다.
      this.todoItems.splice(index, 1);
    },
    toggleComplete() {

    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          this.todoItems.push(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-color: #ffffff;
  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>


위와 같이 수정해놓고 테스트하면 문제점이 보이실겁니다.
로컬스토리지에 데이터는 저장이되는데 화면에 리스트로는 뿌려지지 않습니다.
리스트 뿌리는 컴포넌트는 TodoList 컴포넌트이고 데이터 저장하는 컴포넌트는 TodoInput 컴포넌트입니다.
로컬스토리지에 TodoInput 컴포넌트를 통해 데이터를 넣고나서 새로고침을해줘야지 그때 리스트에 나타나게됩니다. created 훅에 등록한 코드들을 통해..



<!-- src/components/TodoList.vue -->
<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <!-- key 값으로는 primitive 값을 사용해야된다. -->
      <li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem.item" class="shadow">
        <i class="checkBtn fas fa-check" v-on:click="toggleComplete"></i>
        <span v-bind:class="{textCompleted: todoItem.completed}">{{todoItem.item}}</span>
        <span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
          <i class="fas fa-trash-alt"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  methods: {
    removeTodo(todoItem, index) {
      // 여기까지는 로컬스토리지, 쉽게말하면 DB의 영역이다.
      // 스크립트 영역이 아니기 때문에 이렇게 todoItem을 지운다고해서 이것이 바로 화면에 렌더링되진 않는다.
      // 렌더링되게하기 위해선 data의 todoItem 값도 수정해줘야한다.
      localStorage.removeItem(todoItem.item);
      // arr.splice(index, 1): 특정 index로부터 1개를 지운다는 뜻이다. 기존 배열 arr에 변형을 가한다.
      // 반면, slice는 arr.slice()를 해도 arr에 변화를 안가하고 새로운 배열을 반환한다.
      this.todoItems.splice(index, 1);
    },
    toggleComplete() {

    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // 아래와 같이 localStorage 내용을 key값을 통해 불러올 수 있습니다. 그런데 불러와진 값들은 다 string입니다.
          // 왜냐, 저장할 때 JSON.stringify()를 통해 string으로 변형 후 저장했으니까.
          // 그래서 다시 JSON 객체로 돌리기위해 JSON.parse()를 통해 파싱해야된다.
          this.todoItems.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          // this.todoItems.push(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-color: #ffffff;
  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>




<!-- src/components/TodoList.vue -->
<template>
  <div>
    <ul>
      <!-- vue에서 반복문 사용하는 방법 -->
      <!-- v-for를 사용할 땐 v-bind:key 를 작성해주는 것이 좋다. 현재는 todoItem으로 텍스트와 매칭시켜줌 -->
      <!-- 텍스트가 똑같지않은이상 key값은 유일한 값. 사실 더 유일한 값으로 정해주면 더 좋을 것 같다. -->
      <!-- v-for의 성능을 가속화시키는 v-bind:key 값, vue에서 리스트 순서를 정렬하는 방법에 대해서는 나중에 심화로 다루도록 하겠습니다. -->
      <!-- key 값으로는 primitive 값을 사용해야된다. -->
      <li v-for="(todoItem, index) in todoItems" v-bind:key="todoItem.item" class="shadow">
        <i class="checkBtn fas fa-check" v-bind:class="{checkBtnCompleted: todoItem.completed}" v-on:click="toggleComplete(todoItem)"></i>
        <span v-bind:class="{textCompleted: todoItem.completed}">{{todoItem.item}}</span>
        <span class="removeBtn" v-on:click="removeTodo(todoItem, index)">
          <i class="fas fa-trash-alt"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  name: "TodoList",
  data() {
    return {
      todoItems: [],
    }
  },
  methods: {
    removeTodo(todoItem, index) {
      // 여기까지는 로컬스토리지, 쉽게말하면 DB의 영역이다.
      // 스크립트 영역이 아니기 때문에 이렇게 todoItem을 지운다고해서 이것이 바로 화면에 렌더링되진 않는다.
      // 렌더링되게하기 위해선 data의 todoItem 값도 수정해줘야한다.
      localStorage.removeItem(todoItem.item);
      // arr.splice(index, 1): 특정 index로부터 1개를 지운다는 뜻이다. 기존 배열 arr에 변형을 가한다.
      // 반면, slice는 arr.slice()를 해도 arr에 변화를 안가하고 새로운 배열을 반환한다.
      this.todoItems.splice(index, 1);
    },
    toggleComplete(todoItem) {
      // 스크립트상에서의 todoItem의 completed 값만 바꾼 것이다.
      // 로컬스토리지에 저장된 데이터의 값을 바꾸는 것은 아직 작성 안한 상태
      todoItem.completed = !todoItem.completed;

      // 또 하나의 사실 - 로컬스토리지에 업데이트 API가 없습니다.
      // 따라서 삭제하고 다시 등록해야된다. 한마디로 아래 코드는 로컬스토리지 데이터를 갱신하는 코드라고보면됩니다.
      localStorage.removeItem(todoItem.item); // 해당 키로 삭제
      localStorage.setItem(todoItem.item, JSON.stringify(todoItem)); // 키와 값 다시 재등록
    }
  },
  // created: vue life cycle
  // 1. vue instance 생성 - create
  // 2. mounted - 탑재, DOM에 부착
  // 3. update
  // 4. destroy
  // 위 과정에서 Vue는 각각의 단계에서 Vue를 사용하는 사람들을 위해 훅(Hook)을 할 수 있도록 API를 제공합니다.
  // 일반적으로 많이 사용하는 종류로는 beforeCreate, created, beforeMount, mounted, beforeUpdate, updated, beforeDestroy, destroyed가 있습니다.
  // 여기에 추가적인 hook이 2개정도 더 있어서, 총 hook 갯수가 10개정도 될겁니다.
  created() {
    // created는 instance가 생성되자마자 호출되는 라이프사이클 훅이다.
    // 그럼 여기서 할거는 localStorage를 가져올겁니다.
    if (localStorage.length > 0) {
      // 로컬스토리지에 데이터가 있다면 로컬스토리지의 데이터를 꺼내 담습니다.
      for (let i=0; i < localStorage.length; i++) {
        // 조금 원시적인 방법이긴 하지만 아래와 같은 if 문을 추가..
        if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
          // 아래와 같이 localStorage 내용을 key값을 통해 불러올 수 있습니다. 그런데 불러와진 값들은 다 string입니다.
          // 왜냐, 저장할 때 JSON.stringify()를 통해 string으로 변형 후 저장했으니까.
          // 그래서 다시 JSON 객체로 돌리기위해 JSON.parse()를 통해 파싱해야된다.
          this.todoItems.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
          // localStorage에서 제공하는 API입니다. key에 접근할 수 있습니다.
          // this.todoItems.push(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-color: #ffffff;
  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>


2.10 TodoFooter 컴포넌트 구현



<template>
  <div class="clearAllContainer">
    <span class="clearAllBtn" v-on:click="clearTodo">
      Clear All
    </span>
  </div>
</template>

<script>
export default {
  name: "TodoFooter",
  methods: {
    clearTodo() {
      // localStorage의 데이터를 모두 지우는 API clear();
      localStorage.clear();
    }
  }
}
</script>

<style scoped>
.clearAllContainer {
  width: 8.5rem;
  height: 50px;
  line-height: 50px;
  background-color: #ffffff;
  border-radius: 5px;
  margin: 0 auto;
}
.clearAllBtn {
  color: #e20303;
  display: block;
}
</style>


아직 TodoInput에 데이터넣으면 바로바로 TodoList 컴포넌트에 반영되진 않는다.