📋 public/index.html 📋
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.1/css/all.css"
integrity="sha384-fnmOCqbTlWIlj8LyTjo7mOUStjsKC4pOpQbqyi7RrhN7udi9RwhKkMHpvLbHG9Sr" crossorigin="anonymous">
<link href="https://fonts.googleapis.com/css?family=Ubuntu" rel="stylesheet">
📋 src/App.vue 📋
<template>
<div id="app"></div>
</template>
<script></script>
<style></style>
📋 src/components/Todo*.vue 📋
<template>
<div>Header/Input/List/Footer</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
📋 src/App.vue 📋
<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, // 'TodoHheader' : TodoHeader와 동일
TodoInput,
TodoList,
TodoFooter
}
}
</script>
<style></style>
📋 src/components/TodoHeader.vue 📋
<template>
<div>
<h1>TODO it!</h1>
</div>
</template>
<script>
export default {
}
</script>
<style scoped>
h1 {
color:#2F3852;
font-weight: 900;
margin: 2.5rem 0 1.5rem;
}
</style>
📋 src/App.vue 📋
<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 {
name: "App",
components: {
TodoHeader, // 'TodoHheader' : TodoHeader와 동일
TodoInput,
TodoList,
TodoFooter,
},
data() {
return {}
}
};
</script>
<style src="@/assets/styles/styles.css">
</style>
📋 src/assets/styles/styles.css 📋
#app {
font-family: Avenir, Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
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);
}
크롬 개발자 도구
의 Application ➡️ Storage ➡️ Lacal Storage ➡️ http://localhost:8080📋 src/components/TodoInput.vue 📋
<template>
<div class="inputBox shadow">
<!--input 박스에서 enter 입력했을 때도 todo 추가되도록 v-on:keyup.enter 이벤트 처리 -->
<!-- v-on:keyup.enter과 @keyup.enter 동일 -->
<input type="text" v-model="newTodoItem" ref="todoItem" @keyup.enter="addTodo">
<!--awesome 아이콘 이용해 직관적인 버튼 생성-->
<span class="addContainer" v-on:click="addTodo">
<i class="fas fa-plus addBtn"></i>
</span>
</div>
</template>
<script>
export default {
/* $refs는 document.getElementById(id) 함수처럼 html DOM에 직접 접근할 때 사용하는 객체
ref = "todoItem"의 ref 속성은 기존의 id 속성과 동일한 속성 */
mounted() {
this.$refs.todoItem.focus();
},
data() {
return {
newTodoItem: ""
}
},
methods: {
addTodo: function () {
if (this.newTodoItem !== "") {
/* toggleComplete() 메서드를 위한 객체 추가 */
var obj = {completed: false, item: this.newTodoItem};
/* 입력 받은 텍스트를 localStorage의 setItem() API를 이용하여 저장
JSON.stringify()로 object를 json string으로 변환*/
localStorage.setItem(this.newTodoItem, JSON.stringify(obj)); // setItem(key, value)
this.clearInput();
}
},
clearInput: function () {
this.newTodoItem = ""; // newTodoitem 변수 초기화
},
},
};
</script>
<style scoped>
input:focus {
outline: none !important;
box-shadow: 0 0 0.4px #d6a8e9;
}
.inputBox {
background: white;
height: 50px;
line-height: 50px;
border-radius: 5px;
}
.inputBox input {
border-style: none;
font-size: 0.9rem;
width: 75%;
height: 50%;
}
.addContainer {
float: right;
background: linear-gradient(to right, #6478fb, #8763fb);
display: block;
width: 3rem;
border-radius: 0 5px 5px 0;
}
.addBtn {
color: white;
vertical-align: middle;
}
</style>
TodoInput.vue
의 addTodo() 메서드 수정// addTodo 수정 메서드
addTodo: function () {
if (this.newTodoItem !== "") {
/* toggleComplete() 메서드를 위한 객체 추가 */
var obj = {completed: false, item: this.newTodoItem};
/* 입력 받은 텍스트를 localStorage의 setItem() API를 이용하여 저장
JSON.stringify()로 object를 json string으로 변환*/
localStorage.setItem(this.newTodoItem, JSON.stringify(obj)); // setItem(key, value)
this.clearInput();
}
},
📋 src/components/TodoList.vue 📋
<template>
<div>
<ul>
<!-- v-for directive를 사용하여 목록 렌더링 -->
<!-- v-bind 생략 가능 -->
<li v-for="(todoItem, idx) in todoItems" v-bind:key="idx" class="shadow">
<!-- 완료 버튼 마크업 작업 -->
<i class="fas fa-check checkBtn" 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 {
data() {
return {
todoItems: [],
};
},
/* life cycle method */
/* push()로 로컬 스토리지의 모든 데이터를 todoitems에 저장 */
created: function () {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
/* JSON.parse()로 json string을 object로 변환 */
this.todoItems.push(JSON.parse(itemJson));
}
}
},
/* localStorage의 데이터를 삭제하는 removeItem()
배열의 특정 인덱스를 삭제하는 splice() 함수로 todo 삭제 */
methods: {
removeTodo: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
toggleComplete: function(todoItem) {
const { item, completed } = todoItem;
todoItem.completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(todoItem));
/* 상단과 동일
todoItem.completed = !todoItem.completed;
localStorage.removeItem(todoItem.item);
localStorage.setItem(todoItem.item, JSON.stringify(todoItem)); */
}
}
};
</script>
<style scoped>
ul {
list-style-type: none;
padding-left: 0px;
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;
}
.removeBtn {
margin-left: auto;
color: #de4343;
}
.checkBtn {
line-height: 45px;
color: #62acde;
margin-right: 5px;
}
.checkBtnCompleted {
color: #b3adad;
}
.textCompleted {
text-decoration: line-through;
color: #b3adad;
}
</style>
📋 src/components/TodoFooter.vue 📋
<template>
<div class="clearAllContainer">
<span class="clearAllBtn" @click="clearTodo">
<!-- v-on:click과 @click 동일 -->
Clear All
</span>
</div>
</template>
<script>
export default {
methods: {
clearTodo() {
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>
📋 src/components/TodoList.vue 📋
<script>
export default {
props: ["propsdata"],
/* App 으로 이동
data() {
return {
todoItems: []
}
},
// life cycle method
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
*/
methods: {
removeTodo(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
toggleComplete(todoItem) {
const { item, completed } = todoItem;
todoItem.completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(todoItem));
}
},
}
</script>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput></TodoInput>
<TodoList v-bind:propsdata="todoItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
// TodoList에서 이동
data() {
return {
todoItems: []
}
},
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
}
</script>
📋 src/components/TodoInput.vue 📋
<script>
export default {
//LifeCycle Hook method
mounted() {
this.$refs.todoItem.focus();
},
data() {
return {
newTodoItem: ""
}
}, //data
methods: {
addTodo() {
if (this.newTodoItem !== '') {
this.$emit("addItemEvent", this.newTodoItem);
/* app으로 이동
var todoObj = { completed: false, item: this.newTodoItem };
localStorage.setItem(this.newTodoItem, JSON.stringify(todoObj)); */
this.clearInput();
}
}, //addTodo
clearInput() {
this.newTodoItem = '';
}, //clearTodo
}, //methods
}
</script>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
<TodoList v-bind:propsdata="todoItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
// TodoInput에서 이동
methods: {
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
// localStorage와 화면을 동기화 시키기 위해서
this.todoItems.push(obj);
}
}
}
</script>
📋 src/components/TodoList.vue 📋
<script>
export default {
props: ["propsdata"],
methods: {
removeTodo(todoItem, index) {
this.$emit('removeItemEvent', todoItem, index);
/* App 으로 이동
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
*/
},
toggleComplete(todoItem) {
const { item, completed } = todoItem;
todoItem.completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(todoItem));
}
},
}
</script>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
<TodoList v-on:removeItemEvent="removeOneItem" v-bind:propsdata="todoItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
methods: {
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
this.todoItems.push(obj);
},
// TodoList에서 이동
removeOneItem: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
}
}
}
</script>
📋 src/components/TodoList.vue 📋
<script>
export default {
props: ["propsdata"],
methods: {
removeTodo(todoItem, index) {
this.$emit('removeItemEvent', todoItem, index);
},
toggleComplete(todoItem) {
this.$emit('toggleItemEvent', todoItem);
/* App으로 이동
const { item, completed } = todoItem;
todoItem.completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(todoItem));
*/
}
},
}
</script>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
<TodoList v-on:toggleItemEvent="toggleOneItem" v-on:removeItemEvent="removeOneItem" v-bind:propsdata="todoItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
methods: {
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
this.todoItems.push(obj);
},
removeOneItem: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
// TodoList에서 이동
toggleOneItem: function(todoItem) {
this.todoItems[index].completed = !this.todoItems[index].completed;
localStorage.removeItem(todoItem.item);
localStorage.setItem(todoItem.item, JSON.stringify(this.todoItems[index]));
/* completed을 배열로 값 변경
const { item, completed } = todoItem;
todoItem.completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item,JSON.stringify(todoItem));
*/
}
}
}
</script>
📋 src/components/TodoFooter.vue 📋
<script>
export default {
methods: {
clearTodo() {
this.$emit('removeAllItemEvent');
/* App 으로 이동
localStorage.clear();
*/
}
}
}
</script>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
<TodoList v-on:toggleItemEvent="toggleOneItem" v-on:removeItemEvent="removeOneItem" v-bind:propsdata="todoItems"></TodoList>
<TodoFooter v-on:removeAllItemEvent="removeAllItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
methods: {
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
this.todoItems.push(obj);
},
removeOneItem: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
toggleOneItem: function(todoItem, index) {
this.todoItems[index].completed = !this.todoItems[index].completed;
localStorage.removeItem(todoItem.item);
localStorage.setItem(todoItem.item, JSON.stringify(this.todoItems[index]));
},
// TodoFooter에서 이동
removeAllItems: function() {
localStorage.clear();
this.todoItems = [];
}
}
}
</script>
📋 src/components/TodoInput.vue 📋
<template>
<div class="inputBox shadow">
<input type="text" v-model="newTodoItem" ref="todoItem" @keyup.enter="addTodo">
<span class="addContainer" v-on:click="addTodo">
<i class="fas fa-plus addBtn"></i>
</span>
<!-- Modal에 있는 slot의 header와 body 부분만 재정의 -->
<MyModal v-if="showModal" @close="showModal = false">
<h3 slot="header">
경고!
<i class="closeModalBtn fas fa-times" @click="showModal = false"></i>
</h3>
<div slot="body">
아무것도 입력하지 않으셨습니다.
</div>
</MyModal>
</div>
</template>
<script>
/* Modal을 import */
import MyModal from '@/components/common/MyModal.vue';
export default {
mounted() {
this.$refs.todoItem.focus();
},
data() {
return {
newTodoItem: "",
showModal: false
}
},
/* Modal 컴포넌트를 TodoInput의 하위 컴포넌트로 등록 */
components: {
MyModal
},
methods: {
addTodo() {
if (this.newTodoItem !== '') {
//this.$store.commit('addTodo', this.newTodoItem);
const itemObj = { item: this.newTodoItem, completed: false };
this.$store.dispatch('addTodoItem', itemObj);
this.clearInput();
} else {
this.showModal = !this.showModal;
}
},
clearInput() {
this.newTodoItem = '';
},
},
}
</script>
📋 src/components/common/Modal.vue 📋
<template>
<transition name="modal">
<div class="modal-mask">
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header">
default header
</slot>
</div>
<div class="modal-body">
<slot name="body">
default body
</slot>
</div>
<div class="modal-footer">
<slot name="footer">
default footer
<button class="modal-default-button" @click="$emit('close')">
OK
</button>
</slot>
</div>
</div>
</div>
</div>
</transition>
</template>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the modal transition by editing
* these styles.
*/
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>
📋 src/components/TodoList.vue 📋
<template>
<div>
<!-- <ul> 엘리먼트를 <transition-group> 엘리먼트로 변경 -->
<transition-group name="list" tag="ul">
<li v-for="(todoItem, idx) in propsdata" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete(todoItem)"></i>
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<span class="removeBtn" @click="removeTodo(todoItem, idx)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
📋 src/store/store.js 📋
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export const store = new Vuex.Store({
});
📋 src/main.js 📋
import Vue from 'vue'
import App from './App.vue'
// store.js를 import
import { store } from './store/store';
Vue.config.productionTip = false
new Vue({
render: h => h(App),
// store 추가
store,
}).$mount('#app')
상태 : 여러 컴포넌트 간에 공유할 데이터
Todoheader 수정
store 수정
App 수정
<TodoList v-bind:propsdata="todoItems“
의 v-bind 속성을 제거TodoList 수정
📋 src/store/store.js 📋
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
// App에서 이동
const storage = {
fetch() {
const arr = [];
if (localStorage.length > 0) {
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
arr.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
}
return arr;
},
};
export const store = new Vuex.Store({
// state 속성 적용
state: {
todoItems: storage.fetch(),
headerText: "TODO it"
}
});
📋 src/components/TodoHeader.vue 📋
<template>
<header>
<h1>{{this.$store.state.headerText}}</h1>
</header>
</template>
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
<!--v-bind 속성 삭제
<TodoList v-on:toggleItemEvent="toggleOneItem" v-on:removeItemEvent="removeOneItem" v-bind:propsdata="todoItems"></TodoList> -->
<TodoList v-on:toggleItemEvent="toggleOneItem" v-on:removeItemEvent="removeOneItem"></TodoList>
<TodoFooter v-on:removeAllItemEvent="removeAllItems"></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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
/* store로 이동
created() {
if (localStorage.length > 0) {
for (var i = 0; i < localStorage.length; i++) {
var itemJson = localStorage.getItem(localStorage.key(i));
this.todoItems.push(JSON.parse(itemJson));
}
}
},
*/
methods: {
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
this.todoItems.push(obj);
},
removeOneItem: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
toggleOneItem: function(todoItem, index) {
this.todoItems[index].completed = !this.todoItems[index].completed;
localStorage.removeItem(todoItem.item);
localStorage.setItem(todoItem.item, JSON.stringify(this.todoItems[index]));
},
removeAllItems: function() {
localStorage.clear();
this.todoItems = [];
}
}
}
</script>
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<!-- propsdata에서 store 직접 접근으로 변경 -->
<li v-for="(todoItem, idx) in this.$store.state.todoItems" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete(todoItem)"></i>
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<span class="removeBtn" @click="removeTodo(todoItem, idx)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
export default {
/* props 삭제
props: ["propsdata"],
*/
methods: {
removeTodo(todoItem, index) {
this.$emit('removeItemEvent', todoItem, index);
},
toggleComplete(todoItem) {
this.$emit('toggleItemEvent', todoItem);
}
},
}
</script>
state의 값을 변경할 수 있는 메서드
Mutations는 commit()으로 동작
store 수정
App 수정
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput>
➡️ <TodoInput></TodoInput>
로 변경<TodoList v-on:removeItemEvent="removeOneItem"></TodoList>
➡️ <TodoList></TodoList>
로 변경<TodoList v-on:toggleItemEvent="toggleOneItem"></TodoList>
➡️ <TodoList></TodoList>
로 변경<TodoFooter v-on:removeAllItemEvent="removeAllItems"></TodoFooter>
➡️ <TodoFooter></TodoFooter>
로 변경TodoInput 수정
this.$emit('addItemEvent', this.newTodoItem);
➡️ this.$store.commit('addOneItem', this.newTodoItem);
TodoList 수정
this.$emit('removeItemEvent',todoItem,index);
➡️ this.$store.commit('removeOneItem', {todoItem, index});
₩this.$emit('toggleItemEvent',todoItem, index);
➡️ this.$store.commit('toggleOneItem', {todoItem, index});
TodoFooter 수정
this.$emit('removeAllItemEvent');
➡️ this.$store.commit('removeAllItems');
📋 src/store/store.js 📋
export const store = new Vuex.Store({
state: {
todoItems: storage.fetch(),
},
/* mutations 속성 추가 */
mutations: {
/* 할 일 추가 */
addOneItem(state, todoItem) {
const obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
/* 할 일 삭제 */
removeOneItem(state, payload) {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
/*
localStorage.removeItem(payload.todoItem.item);
state.todoItems.splice(payload.index, 1);
*/
},
/* 할 일 완료 */
toggleOneItem(state, payload) {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
/*
state.todoItems[payload.index].completed = !state.todoItems[payload.index].completed;
localStorage.removeItem(payload.todoItem.item);
localStorage.setItem(payload.todoItem.item, JSON.stringify(payload.todoItem));
*/
},
/* 할 일 모두 삭제 */
removeAllItems(state) {
localStorage.clear();
state.todoItems = [];
},
}
});
📋 src/App.vue 📋
<template>
<div id="app">
<TodoHeader></TodoHeader>
<!-- TodoInput v-on 속성 삭제
<TodoInput v-on:addItemEvent="addOneItem"></TodoInput> -->
<TodoInput></TodoInput>
<!-- TodoList v-on 속성 삭제
<TodoList v-on:toggleItemEvent="toggleOneItem" v-on:removeItemEvent="removeOneItem"></TodoList>-->
<TodoList></TodoList>
<!-- TodoFooter v-on 속성 삭제
<TodoFooter v-on:removeAllItemEvent="removeAllItems"></TodoFooter>-->
<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 {
name: 'App',
components: {
TodoHeader,
TodoInput,
TodoList,
TodoFooter
},
data() {
return {
todoItems: []
}
},
methods: {
/* 할 일 추가 메서드 store로 이동
addOneItem: function(todoItem) {
var obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
this.todoItems.push(obj);
},
*/
/* 할 일 삭제 메서드 store로 이동
removeOneItem: function(todoItem, index) {
localStorage.removeItem(todoItem.item);
this.todoItems.splice(index, 1);
},
*/
/* 할 일 완료 메서드 store로 이동
toggleOneItem: function(todoItem, index) {
this.todoItems[index].completed = !this.todoItems[index].completed;
localStorage.removeItem(todoItem.item);
localStorage.setItem(todoItem.item, JSON.stringify(this.todoItems[index]));
},
*/
/* 할 일 모두 삭제 메서드 store로 이동
removeAllItems: function() {
localStorage.clear();
this.todoItems = [];
}
*/
}
}
</script>
📋 src/components/TodoInput.vue 📋
<script>
import MyModal from '@/components/common/MyModal.vue';
export default {
mounted() {
this.$refs.todoItem.focus();
},
data() {
return {
newTodoItem: "",
showModal: false
}
}, //data
components: {
MyModal
},
methods: {
addTodo() {
if (this.newTodoItem !== '') {
/* App에 event 보내는 대신 store에 저장된 메서드 직접 호출
this.$emit('addTodoEvent', this.newTodoItem);
*/
this.$store.commit('addOneItem', this.newTodoItem);
this.clearInput();
} else {
this.showModal = !this.showModal;
}
},
clearInput() {
this.newTodoItem = '';
},
},
}
</script>
📋 src/components/TodoList.vue 📋
<script>
export default {
methods: {
removeTodo(todoItem, index) {
/* App에 event 보내는 대신 store에 저장된 메서드 직접 호출
this.$emit('removeItemEvent', todoItem, index);
*/
this.$store.commit('removeOneItem', {todoItem, index});
},
toggleComplete(todoItem, index) {
/* App에 event 보내는 대신 store에 저장된 메서드 직접 호출
this.$emit('toggleItemEvent', todoItem);
*/
this.$store.commit('toggleOneItem', {todoItem, index});
}
},
}
</script>
📋 src/components/TodoFooter.vue 📋
<script>
export default {
methods: {
clearTodo() {
/* App에 event 보내는 대신 store에 저장된 메서드 직접 호출
this.$emit('removeAllItemEvent');
*/
this.$store.commit('removeAllItems');
}
}
}
</script>
📖 참고 📖 state를 직접 변경하지 않고, mutations 변경 이유
- 여러 개의 컴포넌트에서 state 값 변경하는 경우, 어느 컴포넌트에서 해당 state를 변경했는지 추적하기가 어려움
- 🗒️ 예시
methods: { increaseCounter() { this.$store.state.counter++ } }
- 특정 시점에 어떤 컴포넌트가 state를 접근하여 변경한 것인지 확인하기 어려움
➡️ 뷰의 반응성을 거스르지 않게 명시적으로 상태 변화 수행
//App.vue
import { mapState } from 'vuex'
import { mapGetters } from 'vuex'
import { mapMutations } from 'vuex'
import { mapActions } from 'vuex'
export default {
computed: { ...mapState(['num']), ...mapGetters(['countedNum']) },
methods:
{ ...mapMutations(['clickBtn']), ...mapActions(['asyncClickBtn']) }
}
<p>{{this.$store.state.num}}</p>
➡️ <p>{{num}}</p>
//App.vue
import { mapState } from 'vuex'
export default {
computed: { ...mapState(['num']) }
//num() { return this.$store.state.num }
}
//store.js
state: {
num: 10
}
<p>{{this.$store.getters.reverseMessage}}</p>
➡️ <p>{{reverseMessage}}</p>
//App.vue
import { mapGetters } from 'vuex'
export default {
computed : { ...mapGetters(['reverseMessage']) }
}
//store.js
getters: {
reverseMessage(state) {
return state.msg.split('').reverse().join('');
}
}
<button v-on:click="setValue">변경</button>
//App.vue
import { mapMutations } from 'vuex'
export default {
methods : {
...mapMutations(['setValue']),
authLogin() {},
displayTable() {}
}
}
//store.js
mutations: {
setValue(state, value) {
this.values += value
}
}
<button @click="delayClickBtn">delay popup message</button>
//App.vue
import { mapActions } from 'vuex'
export default {
methods : {
...mapActions(['delayClickBtn']),
}
}
//store.js
actions: {
delayClickBtn(context) {
setTimeout(() => context.commit('clickBtn'), 2000);
}
}
//배열 리터럴
export default {
methods : {
...mapMutations(['clickBtn', 'addNumber']),
}
}
//객체 리터럴
export default {
methods : {
...mapMutations({ popupMsg : 'clickBtn' }),
}
}
📋 src/store/store.js 📋
export const store = new Vuex.Store({
/* getters 추가 */
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
...
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<!-- this.$store.state.todoItems를 this.$store.getters.getTodoItems로 변경 -->
<!-- this.$store.getters.getTodoItems에서 todoItems로 변경 -->
<li v-for="(todoItem, idx) in todoItems" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete(todoItem, idx)"></i>
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<span class="removeBtn" @click="removeTodo(todoItem, idx)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
export default {
methods: {
removeTodo(todoItem, index) {
this.$store.commit('removeOneItem', {todoItem, index});
},
toggleComplete(todoItem, index) {
this.$store.commit('toggleOneItem', {todoItem, index});
}
},
/* computed 속성 추가 */
computed: {
todoItems() {
return this.$store.getters.getTodoItems;
}
},
}
</script>
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<!-- todoItems를 computed 속성의 getTodoItems로 변경 -->
<li v-for="(todoItem, idx) in getTodoItems" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete(todoItem, idx)"></i>
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<span class="removeBtn" @click="removeTodo(todoItem, idx)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
/* mapGetters를 import */
import { mapGetters } from 'vuex'
export default {
methods: {
removeTodo(todoItem, index) {
this.$store.commit('removeOneItem', {todoItem, index});
},
toggleComplete(todoItem, index) {
this.$store.commit('toggleOneItem', {todoItem, index});
}
},
computed: {
...mapGetters(['getTodoItems'])
/* 전개 연산자(spread operator)를 사용하여 mapGetters 선언
todoItems() {
return this.$store.getters.getTodoItems;
}*/
},
}
</script>
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<!-- todoItems를 computed 속성의 getTodoItems로 변경 -->
<li v-for="(todoItem, idx) in getTodoItems" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete({todoItem, idx})"></i>
<!-- toggleComplete(todoItem, idx)의 인자 타입을 toggleComplete({ todoItem, idx }) 객체로 수정 -->
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<!-- removeTodo(todoItem, idx)의 인자 타입을 removeTodo({ todoItem, idx }) 객체로 수정 -->
<span class="removeBtn" @click="removeTodo({todoItem, idx})">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
/* mapMutations를 import */
import { mapGetters, mapMutations } from 'vuex'
export default {
methods: {
...mapMutations({
removeTodo: 'removeOneItem',
toggleComplete: 'toggleOneItem',
}),
/* spread operator 사용하여 mapMutations 선언
removeTodo(todoItem, index) {
this.$store.commit('removeOneItem', {todoItem, index});
},
*/
/* spread operator 사용하여 mapMutations 선언
toggleComplete(todoItem, index) {
this.$store.commit('toggleOneItem', {todoItem, index});
}
*/
},
computed: {
...mapGetters(['getTodoItems'])
/* 전개 연산자(spread operator)를 사용하여 mapGetters 선언
todoItems() {
return this.$store.getters.getTodoItems;
}*/
},
}
</script>
📋 src/components/TodoFooter.vue 📋
<template>
<div class="clearAllContainer">
<span class="clearAllBtn" @click="clearTodo">Clear All</span>
</div>
</template>
<script>
/* mapMutations를 import */
import { mapMutations } from 'vuex'
export default {
methods: {
...mapMutations({
clearTodo: 'removeAllItems'
}),
/* pread operator 사용하여 mapMutations 선언
clearTodo() {
this.$store.commit('removeAllItems');
}
*/
}
}
</script>
//app.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello World!');
});
app.listen(3000, function () {
console.log('Example app listening on port 3000!');
});
📋 src/store/store.js 📋
import Vue from 'vue';
import Vuex from 'vuex';
// axios와 vue-axios import
import axios from 'axios';
import VueAxios from 'vue-axios';
Vue.use(Vuex);
Vue.use(VueAxios, axios); // 순서 중요
/*
const storage = {
fetch() {
const arr = [];
if (localStorage.length > 0) {
for (let i = 0; i < localStorage.length; i++) {
if (localStorage.key(i) !== 'loglevel:webpack-dev-server') {
arr.push(JSON.parse(localStorage.getItem(localStorage.key(i))));
}
}
}
return arr;
},
};
*/
const todo_url = 'http://localhost:4500/api/todos';
export const store = new Vuex.Store({
state: {
todoItems: []
/* state의 todoItems 변수를 초기화
todoItems: storage.fetch(),
*/
},
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
/* actions 속성의 loadTodoItems()에서 axios.get() 호출 */
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
mutations: {
/* setTodoItems 추가 */
setTodoItems(state, items) {
state.todoItems = items;
},
addOneItem(state, todoItem) {
const obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
removeOneItem(state, payload) {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
},
toggleOneItem(state, payload) {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
},
removeAllItems(state) {
localStorage.clear();
state.todoItems = [];
},
},
});
📋 src/components/TodoList.vue 📋
<script>
import { mapGetters, mapMutations } from 'vuex'
export default {
mounted () {
this.$store.dispatch('loadTodoItems');
},
methods: {
...mapMutations({
removeTodo: 'removeOneItem',
toggleComplete: 'toggleOneItem',
}),
},
computed: {
...mapGetters(['getTodoItems'])
},
}
</script>
📋 src/store/store.js 📋
export const store = new Vuex.Store({
state: {
todoItems: []
},
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
/* actions 속성의 removeTodoItem()에서 axios.delete() 호출 */
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
...
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, idx) in getTodoItems" :key="idx" class="shadow">
<i class="fas fa-check checkBtn" :class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleComplete({todoItem, idx})"></i>
<span :class="{ textCompleted: todoItem.completed }">{{ todoItem.item }}</span>
<!-- removeTodo({todoItem, idx})객체를 removeTodoItem(todoItem)로 변경 -->
<!-- <span class="removeBtn" @click="removeTodo({todoItem, idx})"> -->
<span class="removeBtn" @click="removeTodoItem(todoItem)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
/* mapAcitions를 import */
import { mapGetters, mapMutations, mapActions } from 'vuex'
export default {
mounted () {
this.$store.dispatch('loadTodoItems');
},
methods: {
...mapMutations({
removeTodo: 'removeOneItem',
toggleComplete: 'toggleOneItem',
}),
/* methods에 mapActions 추가 */
...mapActions(['removeTodoItem']),
},
computed: {
...mapGetters(['getTodoItems'])
},
}
</script>
📋 src/store/store.js 📋
export const store = new Vuex.Store({
state: {
todoItems: []
},
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
/* actions 프로퍼티의 addTodoItem() 에서 axios.post() 을 호출 */
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
...
📋 src/components/TodoInput.vue 📋
<script>
import MyModal from '@/components/common/MyModal.vue';
export default {
mounted() {
this.$refs.todoItem.focus();
},
data() {
return {
newTodoItem: "",
showModal: false
}
}, //data
components: {
MyModal
},
methods: {
addTodo() {
if (this.newTodoItem !== '') {
const itemObj = { completed: false, item: this.newTodoItem };
this.$store.dispatch('addTodoItem', itemObj);
/* commit에서 dispatch로 수정
this.$store.commit('addOneItem', this.newTodoItem);
*/
this.clearInput();
} else {
this.showModal = !this.showModal;
}
},
clearInput() {
this.newTodoItem = '';
},
},
}
</script>
📋 src/store/store.js 📋
export const store = new Vuex.Store({
state: {
todoItems: []
},
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
/* actions 프로퍼티의 removeAllTodoItems() 에서 axios.delete() 을 호출 */
removeAllTodoItems(context) {
axios
.delete(`${todo_url}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
...
📋 src/components/TodoFooter.vue 📋
<template>
<div class="clearAllContainer">
<!-- clearTodo를 removeAllTodoItems로 변경 -->
<span class="clearAllBtn" @click="clearTodo">Clear All</span>
</div>
</template>
<script>
/* mapActions를 import*/
import { mapMutations, mapActions } from 'vuex'
export default {
methods: {
...mapMutations({
clearTodo: 'removeAllItems'
}),
/* mapActions 추가 */
...mapActions(['removeAllTodoItems']),
}
}
</script>
📋 src/store/store.js 📋
export const store = new Vuex.Store({
state: {
todoItems: []
},
getters: {
getTodoItems(state) {
return state.todoItems;
}
},
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeAllTodoItems(context) {
axios
.delete(`${todo_url}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
/* actions 프로퍼티의 toggleTodoItem() 에서 axios.put() 을 호출 */
toggleTodoItem(context, payload) {
axios
.patch(`${todo_url}/${payload.id}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
...
📋 src/components/TodoList.vue 📋
<template>
<div>
<transition-group name="list" tag="ul">
<li v-for="(todoItem, idx) in getTodoItems" :key="idx" class="shadow">
<i
class="fas fa-check checkBtn"
:class="{ checkBtnCompleted: todoItem.completed }"
@click="toggleTodo(todoItem)"
></i>
<!-- toggleComplete({todoItem, idx}객체를 toggleTodo(todoItem)로 변경 -->
<span :class="{ textCompleted: todoItem.completed }">{{
todoItem.item
}}</span>
<span class="removeBtn" @click="removeTodoItem(todoItem)">
<i class="fas fa-trash-alt"></i>
</span>
</li>
</transition-group>
</div>
</template>
<script>
/* mapAcitions를 import */
import { mapGetters, mapMutations, mapActions } from "vuex";
export default {
mounted() {
this.$store.dispatch("loadTodoItems");
},
methods: {
...mapMutations({
removeTodo: "removeOneItem",
toggleComplete: "toggleOneItem",
}),
...mapActions(["removeTodoItem"]),
/* methods에 toggleTodo 추가 */
toggleTodo(todoItem) {
todoItem.completed = !todoItem.completed;
this.$store.dispatch("toggleTodoItem", todoItem);
},
},
computed: {
...mapGetters(["getTodoItems"]),
},
};
</script>
npm run serve
npm run build
npm run test
//package.json
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
// local 로컬 모드 추가
"local": "vue-cli-service serve --mode local",
// 사용자 정의 모드 생성
"mymode": "vue-cli-service serve --mode mymode",
},
📋 .env.development 📋
// npm run serve
VUE_APP_TITLE=개발 모드
VUE_APP_APIURL=http://localhost:4500/api
📋 src/store/store.js 📋
/* api_url 추가 */
const api_url = process.env.VUE_APP_APIURL;
const todo_url = `${api_url}/todos`
// const todo_url = 'http://localhost:4500/api/todos';
📋 .env.production 📋
// npm run build
VUE_APP_TITLE=운영 모드
VUE_APP_APIURL=http://localhost:4500/api
📋 src/components/Todo.vue 📋
<template>
<header>
<h1>TODO it {{ mode }}</h1>
</header>
</template>
<script>
export default {
setup() {
const mode = process.env.VUE_APP_TITLE
return {mode}
}
}
</script>
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
export const store = new Vuex.Store({
state: {}
getters: {},
mutations: {},
actions: {}
});
//store.js
import Vue from 'vue'
import Vuex from 'vuex'
import todo from 'modules/todo.js'
export const store = new Vuex.Store({
modules: {
//모듈명칭 : 모듈파일명 todo:todo
todo
}
});
//todo.js
const state = {}
const getters = {}
const mutations = {}
const actions = {}
📋 src/store/getters.js 📋
export const storedTodoItems = (state) => {
return state.todoItems;
}
/* 기존 코드
getTodoItems(state) {
return state.todoItems;
}
*/
📋 src/store/mutations.js 📋
const addOneItem = (state, todoItem) => {
const obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
}
const removeOneItem = (state, payload) => {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
}
const toggleOneItem = (state, payload) => {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
}
const removeAllItems = (state) => {
localStorage.clear();
state.todoItems = [];
}
export { addOneItem, removeOneItem, toggleOneItem, removeAllItems }
/* 기존 코드
setTodoItems(state, items) {
state.todoItems = items;
},
addOneItem(state, todoItem) {
const obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
removeOneItem(state, payload) {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
},
toggleOneItem(state, payload) {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
},
removeAllItems(state) {
localStorage.clear();
state.todoItems = [];
},
*/
📋 src/store/store.js 📋
/* getters.js와 mutations.js를 import */
import * as getters from './getters';
import * as mutations from './mutations';
export const store = new Vuex.Store({
state: {
todoItems: Storage.fetch()
/*
todoItems: []
*/
},
getters: {
getters
/* getters로 이동
getTodoItems(state) {
return state.todoItems;
}
*/
},
mutations: {
mutations
/* mutations로 이동
setTodoItems(state, items) {
state.todoItems = items;
},
addOneItem(state, todoItem) {
const obj = { completed: false, item: todoItem };
localStorage.setItem(todoItem, JSON.stringify(obj));
state.todoItems.push(obj);
},
removeOneItem(state, payload) {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
},
toggleOneItem(state, payload) {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
},
removeAllItems(state) {
localStorage.clear();
state.todoItems = [];
},
*/
},
📋 src/store/store.js 📋
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import VueAxios from 'vue-axios';
/* todo를 import */
import todo from './modules/todo';
Vue.use(Vuex);
Vue.use(VueAxios, axios);
// const api_url = process.env.VUE_APP_APIURL;
// const todo_url = `${api_url}/todos`
// import * as getters from './getters';
// import * as mutations from './mutations';
export const store = new Vuex.Store({
modules: {
todo // todo: todo
}
/* todo.js로 이동
state: {
todoItems: Storage.fetch()
},
getters: {
getters
},
mutations: {
mutations
},
actions: {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeAllTodoItems(context) {
axios
.delete(`${todo_url}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
toggleTodoItem(context, payload) {
axios
.patch(`${todo_url}/${payload.id}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
},
*/
});
📋 src/store/modules/todo.js 📋
import axios from 'axios';
const api_url = process.env.VUE_APP_APIURL;
const todo_url = `${api_url}/todos`
//'http://localhost:4500/api/todos';
const state = {
todoItems: []
};
const getters = {
getTodoItems(state) {
return state.todoItems;
}
}; //getters
const actions = {
loadTodoItems(context) {
axios
.get(`${todo_url}`) //Promise
.then(res => res.data)
.then(items => {
context.commit('setTodoItems', items)
})
}, //loadTodoItems
addTodoItem(context, payload) {
axios
.post(`${todo_url}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
}, //addTodoItem
removeTodoItem(context, payload) {
axios
.delete(`${todo_url}/${payload.id}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
}, //removeTodoItem
toggleTodoItem(context, payload) {
axios
.patch(`${todo_url}/${payload.id}`, payload)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
removeAllTodoItems(context) {
axios
.delete(`${todo_url}`)
.then(r => r.data)
.then(items => {
context.commit('setTodoItems', items)
})
},
}; //actions
const mutations = {
setTodoItems(state, items) {
state.todoItems = items;
},
addTodo(state, todo_text) {
const obj = { completed: false, item: todo_text };
localStorage.setItem(todo_text, JSON.stringify(obj));
state.todoItems.push(obj);
},
removeTodo(state, payload) {
const { todoItem: { item }, index } = payload;
localStorage.removeItem(item);
state.todoItems.splice(index, 1);
},
toggleTodo(state, payload) {
const { todoItem: { item, completed }, index } = payload;
state.todoItems[index].completed = !completed;
localStorage.removeItem(item);
localStorage.setItem(item, JSON.stringify(state.todoItems[index]));
},
clearTodo(state) {
localStorage.clear();
state.todoItems = [];
},
}; //mutations
export default {
state, getters, actions, mutations
}
<router-link to="URL 값">
: 페이지 이동 태그, 화면에서는 <a>로 표시되며 클릭하며 to에 지정한 URL로 이동<router-view>
: 페이지 표시 태그, 변경되는 URL에 따라 해당 컴포넌트를 뿌려주는 영역📋 src/main.js 📋
import Vue from 'vue'
import App from './App.vue'
import { store } from './store/store';
// router를 import
import router from './router'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
store,
// router 추가
router
}).$mount('#app')
📋 src/router/index.js 📋
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
📋 src/App.vue 📋
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</nav>
<router-view/>
</div>
</template>
📋 src/views/Posts.vue 📋
<template>
<div>
<h1>Posts</h1>
<router-view></router-view>
</div>
</template>
📋 src/views/PostNew.vue 📋
<template>
<div>
<h1>Post New page</h1>
</div>
</template>
📋 src/views/PostDetail.vue 📋
<template>
<div>
<h1>Post Detail page</h1>
</div>
</template>
📋 src/router/index.js 📋
import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
/* posts를 import */
import Posts from '@/views/PostList.vue'
import PostNew from '@/views/PostNew.vue'
import PostDetail from '@/views/PostDetail.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: HomeView
},
{
path: '/about',
name: 'about',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/AboutView.vue')
},
/* PostList, PostNew, PostDetail 추가 */
{
path: '/posts', component: PostList,
children: [
{ path: 'new', component: PostNew },
{ path: ':id', component: PostDetail, name: 'post' },
]
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
📋 src/App.vue 📋
<template>
<div id="app">
<nav>
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
<!-- posts 속성 추가 -->
<router-link to="/posts">Posts</router-link> |
<router-link to="/posts/new" exact>New Post</router-link>
</nav>
<router-view />
</div>
</template>
📋 src/router/index.js 📋
export default new Router({
routes: [
{ path: '/posts', component: Posts,
children: [
{ path: 'new', component: PostNew},
{ path: ':id', name: 'post',
component: PostDetail }
]
}
]
})
📋 src/views/PostDetail.vue 📋
<template>
<div>
<h1>This is an id: {{route.params.id}}
Post Detail page</h1>
</div>
</template>
📋 src/App.vue 📋
<style>
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
}
nav {
padding: 30px;
}
nav a {
font-weight: bold;
color: #2c3e50;
}
nav a.router-link-exact-active {
color: #42b983;
}
</style>
📋 src/store/modules/post.js 📋
import axios from "axios";
const api_url = process.env.VUE_APP_APIURL;
const post_url = `${api_url}/posts`;
const state = {
posts: [],
post: {},
};
const getters = {
getPosts(state) {
return state.posts;
},
getPost(state) {
return state.post;
},
};
const actions = {
loadPosts(context) {
axios
.get(`${post_url}`)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
loadPost(context, payload) {
axios
.get(`${post_url}/${payload.id}`)
.then((res) => res.data)
.then((item) => context.commit("setPost", item))
.catch((err) => console.error(err));
},
removePost(context, id) {
axios
.delete(`${post_url}/${id}`)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
addPost(context, payload) {
axios
.post(`${post_url}`, payload)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
};
const mutations = {
setPosts(state, items) {
state.posts = items;
},
setPost(state, item) {
state.post = item;
},
};
export default {
state,
getters,
actions,
mutations,
};
📋 src/store/modules/modulePost.js 📋
const actions = {
loadPosts(context) {
axios
.get(`${post_url}`)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
loadPost(context, payload) {
axios
.get(`${post_url}/${payload.id}`)
.then((res) => res.data)
.then((item) => context.commit("setPost", item))
.catch((err) => console.error(err));
},
removePost(context, id) {
axios
.delete(`${post_url}/${id}`)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
addPost(context, payload) {
axios
.post(`${post_url}`, payload)
.then((res) => res.data)
.then((items) => context.commit("setPosts", items))
.catch((err) => console.error(err));
},
};
export default {
state, getters, actions, mutations,
};
📋 src/store/store.js 📋
import Vue from 'vue';
import Vuex from 'vuex';
import axios from 'axios';
import VueAxios from 'vue-axios';
import todo from './modules/todo';
/* post를 import */
import post from './modules/post';
Vue.use(Vuex);
Vue.use(VueAxios, axios);
export const store = new Vuex.Store({
modules: {
todo,
/* post 추가 */
post
}
});
📋 src/views/PostList.vue 📋
<template>
<div class="posts">
<h1>Posts</h1>
<div v-if="loading">Loading...</div>
<div v-for="post in getPosts" :key="post.id">
<router-link :to="{ name: 'post', params: { id: post.id } }">
[ID: {{ post.id }}] {{ post.text | summary }}
</router-link>
</div>
<router-view></router-view>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
data() {
return { loading: true };
},
//lifecycle method
created() {
this.fetchData();
},
filters: {
summary(val) {
return val.substring(0, 20) + "...";
},
},
computed: {
...mapGetters(['getPosts'])
},
methods: {
fetchData() {
this.loading = true;
this.$store.dispatch('loadPosts').then(() => {
this.loading = false;
});
},
},
};
</script>
📋 src/views/PostDetail.vue 📋
<template>
<div>
<h2>View Post</h2>
<div v-if="loading">Loading...</div>
<div v-if="getPost">
<h3>[ID: {{ getPost.id }}]</h3>
<div>{{ getPost.text }}</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
data() {
return {
loading: true,
};
},
created() {
this.fetchData();
},
watch: { $route: "fetchData" },
computed: {
...mapGetters(["getPost"]),
},
methods: {
fetchData() {
this.loading = true;
this.$store
.dispatch("loadPost", { id: this.$route.params.id })
.then(() => {
this.loading = false;
});
},
},
};
</script>
<style scoped>
button {
margin: 1rem 0;
}
</style>
📋 src/views/PostNew.vue 📋
<template>
<div>
<h2>New Post</h2>
<form @submit.prevent="onSubmit">
<textarea
cols="30"
rows="10"
v-model="inputTxt"
:disabled="disabled"
></textarea
><br />
<input type="submit" :value="btnTxt" :disabled="disabled" />
</form>
</div>
</template>
<script>
export default {
data() {
return {
isSaving: false,
inputTxt: "",
};
},
computed: {
btnTxt() {
return this.isSaving ? "Saving..." : "Save";
},
disabled() {
return this.isSaving;
},
},
methods: {
onSubmit() {
this.isSaving = true;
const post = { text: this.inputTxt };
this.$store.dispatch("addPost", post).then(() => {
this.isSaving = false;
this.inputTxt = "";
this.$router.push("/posts");
});
},
},
};
</script
>
<style scoped>
input {
margin: 1rem 0;
}
</style>