Vue + SpringBoot + Mysql 를 이용한 Todo 구현(1) - 프로젝트 구성하기
Vue + SpringBoot + Mysql 를 이용한 Todo 구현(2) - 프로젝트 환경 설정하기
Vue + SpringBoot + Mysql 를 이용한 Todo 구현(3) - Todo API 개발하기(1)
Vue + SpringBoot + Mysql 를 이용한 Todo 구현(4) - Todo API 개발하기(2)
Vue + SpringBoot + Mysql 를 이용한 Todo 구현(5) - Todo 화면 개발하기(1) - 컴포넌트 구성
Vue + SpringBoot + Mysql 를 이용한 Todo 구현(6) - Todo 화면 개발하기(2) - Vuex 적용
이전 글에서 Vue.js 에서 서버와 통신하여 데이터를 가져오는 것까지 확인하였습니다.
이번 글에서는 그 데이터를 각 컴포넌트에 가져와서 컴포넌트에서 해당 데이터를 관리하도록 화면 개발을 진행하도록 해보겠습니다.
6. UX 개편하기
- 필수적인 기능은 아니지만, 편의성을 위해 추가하였습니다.
6-1. 이름 저장하기(App.vue)
- 해당 사용자가 누구인지(물론 자기 자신일거지만...) 물어보는 화면을 추가합니다.
- 해당 화면은 처음 진입시에만 보이도록 설정하여, 보임/숨김 처리하도록 합니다.
- Vue.js 에서 v-if 를 사용하여 , 조건부 렌더링을 합니다.
- App.vue 파일의 HTML 소스를 수정합니다.
<!-- App.vue -->
<template>
<div id="app">
<div class="top">
<TodoHeader />
<div v-if="this.storedName">
<TodoTitle />
<TodoInput v-on:alertModal="showModal" @reload="reload"/>
</div>
<div v-else>
<TodoHello />
</div>
</div>
<div class="body">
<div v-if="this.storedName">
<TodoController />
<TodoList ref="list" />
</div>
<TodoFooter />
</div>
<TodoModal v-show="modalVisible" v-on:close="modalVisible = false">
<template v-slot:modal-text>{{ modalText }}</template>
</TodoModal>
</div>
</template>
- storedName(=userName)을 조회하여, 만약 존재하지 않는다면, <TodoHello />가 표시됩니다.
6-2. TodoHello.vue
- TodoHello.vue 컴포넌트는 이름을 물어보고, session에 저장합니다.
<!-- TodoHello.vue -->
<template>
<div className="hello">
<p className="hello__guide">
<span className="hello__guide-text">Nice to meet you!</span>
<span className="hello__guide-text">I’m going to remember your tasks.</span>
</p>
<p className="hello__ask">What is your name?</p>
<label htmlFor="user-name">Name</label>
<div className="main-input">
<input
className="hello__input"
type="text"
id="user-name"
placeholder="Let me know your name"
v-model="userName"
v-on:keypress.enter="addUserName(userName)"
/>
<button className="hello__button">
<span className="blind">Enter</span>
</button>
</div>
</div>
</template>
<script>
import {mapMutations} from "vuex";
export default {
data() {
return {
userName: ""
};
},
methods: {
...mapMutations({
addUserName: "setUserName"
})
}
};
</script>
<style>
...
</style>
- Vuex 의 mutations 를 통해서 UserName 을 전달하여, Session에 저장합니다.
6-3. App.vue
- 6-1에서 HTML 소스를 정리하였다면, Script 도 정리합니다.
- 6-2에서 UserName을 저장하면서, Session 에 이름이 저장되어, 해당 값을 조회합니다.
// App.vue
<script>
import TodoHeader from "./components/TodoHeader";
import TodoTitle from "./components/TodoTitle";
import TodoInput from "./components/TodoInput";
import TodoController from "./components/TodoController";
import TodoList from "./components/TodoList";
import TodoFooter from "./components/TodoFooter";
import TodoHello from "./components/TodoHello";
import TodoModal from "./components/common/TodoModal";
import { mapGetters } from "vuex";
export default {
name: "App",
data() {
return {
modalVisible: false,
modalText: ""
};
},
computed: {
...mapGetters(["storedName"])
},
methods: {
showModal(text) {
this.modalText = text;
this.modalVisible = !this.modalVisible;
},
reload(){
this.$refs.list.getBoardList();
}
},
components: {
TodoHeader,
TodoTitle,
TodoInput,
TodoController,
TodoList,
TodoFooter,
TodoHello,
TodoModal
}
};
</script>
<style>
...
</style>
- 두 작업이 진행되면, 아래 그림과 같이 동작합니다.
- 처음 화면을 진입하면, 이름을 묻고, 입력하게 되면, Todo 화면이 나오게 됩니다.
7. 화면 수정하기
7-1. TodoTitle.vue
- 시간대별 인사(시간대 별 Moring, Afternoon, evening)를 보여준다.
- 유저 이름(UserName)을 보여주고, 수정 가능케 한다.
- 오늘의 할 일(Todo)의 개수 현황을 보여준다.
<!-- TodoTitle.vue -->
<template>
<div class="title">
<p class="title__text">
<span class="title__message">Good {{ message }},</span>
<span
v-on:keyup.enter="handleEnter"
v-on:blur="handleBlur"
class="title__name"
ref="test"
contenteditable="true"
>{{ this.userName }}</span>
.
</p>
<p class="title__task">
<span class="title__task-top">오늘의 할 일은</span>
<span class="title__task-count">
<em class="title__task-left">{{ this.todoItemsCount.left }}</em>
<em
v-if="this.todoItemsCount.total"
class="title__task-total"
> / {{ this.todoItemsCount.total }}</em>
</span>
<span class="title__task-bottom">
<span v-if="this.todoItemsCount.total > 1"></span>
<span v-else></span> 입니다 !
</span>
<span class="title__task-info"></span>
</p>
</div>
</template>
<script>
import getDate from "../assets/common/getDate.js";
export default {
data() {
return {
//시간대별로 morning, afternoon, evening 출력
message: "",
// 저장된 userName 가져옴
userName: this.$store.getters.storedName
};
},
methods: {
// 포커스아웃시, 입력된 이름을 userName으로 저장
handleBlur(e) {
const originalName = this.userName;
const newName = e.target.innerText;
if (newName !== originalName) {
if (newName === "") {
e.target.innerText = originalName;
} else {
this.$store.commit("setUserName", newName);
}
}
},
handleEnter() {
this.$refs.test.blur();
}
},
computed: {
// 할 일 목록 수 표시
todoItemsCount() {
const checkLeftItems = () => {
const items = this.$store.getters.storedTodoItems;
let leftCount = 0;
for (let i = 0; i < items.length; i++) {
if (items[i].completed === false) {
leftCount++;
}
}
return leftCount;
};
const count = {
total: this.$store.getters.storedTodoItemsCount,
left: checkLeftItems()
};
return count;
}
},
mounted() {
this.message = getDate().daytime;
}
};
</script>
- 해당 화면에서는 vuex를 사용하지 않고, 일반 vue.js가 제공하는 방법으로 데이터를 가져와 봤습니다.
- computed
- this.$store.getters.storedTodoItems : Todo 목록의 아이템들을 가져온다.
- mounted()
- getDate.js 에서 시간을 가져와 오전 오후 저녁에 대한 메시지를 보여준다.
7-2. TodoList.vue
- Todo 의 목록을 보여준다.
- 목록이 없을 경우, class를 추가하여 css 로 빈 화면을 보여준다.
<!-- TodoList.vue -->
<template>
<transition-group name="list" tag="ul" class="list" v-bind:class="listempty">
<li
class="list__item"
v-for="(todoItem, index) in this.storedTodoItems"
v-bind:key="todoItem.item"
>
<input
type="checkbox"
v-bind:id="todoItem.item"
v-bind:checked="todoItem.completed === true"
v-on:change="toggleComplete({todoItem})"
/>
<label v-bind:for="todoItem.item" class="list__label">
<span class="icon-check"></span>
<p class="list__text">{{ todoItem.item }}</p>
</label>
<div class="list__right">
<button class="list__delete" v-on:click="removeTodo({todoItem, index})">
<div class="blind">Delete</div>
</button>
<p class="list__date">{{ todoItem.date }}</p>
</div>
</li>
</transition-group>
</template>
<script>
import {mapGetters} from "vuex";
import {mapMutations} from "vuex";
export default {
name: 'todoItems',
computed: {
...mapGetters(["storedTodoItems", "storedTodoItemsCount"]),
listempty() {
return this.storedTodoItemsCount <= 0 ? "list--empty" : null;
}
},
methods: {
...mapMutations({
removeTodo: "removeOneItem",
toggleComplete: "toggleOneItem"
}),
},
};
</script>
<style>
...
</style>
- 예를들어, vuex를 통해 데이터를 가져오는 순서는 다음과 같다.
1. TodoList.vue 에서 mapGetters 를 통해 storedTodoItems를 호출
2. store.js 에서 getter를 조회 -> getter.js
3. getters.js 에서 호출한 storedTodoItems 조회
- coumputed
- mapGetters : vuex를 통해 저장된 데이터를 가져온다.(storedTodoItems, storedTodoItemsCount)
- listempty() : 목록이 없다면, list--empty class를 추가하여, 빈 화면에 대한 css 를 보여준다.
- methods
- removeTodo : vuex를 통해 선택한 Todo 를 지운다.(removeOneItem)
- toggleComplete : vuex를 통해 선택한 Todo 상태를 변경한다. (toggleOneItem)
7-3. TodoInput.vue
- Todo 아이템을 입력 받는다.
- 중복이 있을 경우 모달(Modal)창을 띄워 알린다.
<!-- TodoInput.vue -->
<template>
<div class="add">
<div class="main-input">
<input
type="text"
class="add__input"
placeholder="Enter your task here"
v-model="newTodoItem"
v-on:keypress.enter="addTodoItem"
ref="taskInput"
/>
<button class="add__buttonn" v-on:click="addTodoItem">
<span class="blind">Add</span>
</button>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
data() {
return {
newTodoItem: ""
};
},
computed: {
...mapGetters(["storedTodoItems", "storedTodoItemsCount"])
},
methods: {
addTodoItem() {
// 중복되는 내용인 경우
const oldItems = this.storedTodoItems;
for (let i = 0; i < this.storedTodoItemsCount; i++) {
if (oldItems[i].item === this.newTodoItem) {
const text = "I think you've already had the task.";
this.$emit("alertModal", text);
return false;
}
}
// 빈 내용인 경우
if (this.newTodoItem === "") {
const text = "The form is empty. Please note your task.";
this.$emit("alertModal", text);
this.clearInput();
return false;
}
this.$store.commit("addOneItem", this.newTodoItem);
this.clearInput();
this.$refs.taskInput.focus();
},
clearInput() {
this.newTodoItem = "";
}
}
};
</script>
<style>
...
</style>
- coumputed
- mapGetters : vuex를 통해 저장된 데이터를 가져온다.(storedTodoItems, storedTodoItemsCount)
- methods
- addTodoItem : vuex를 통해 입력한 Todo 를 추가한다.(addOneItem)
- clearInput : Todo 를 입력 후에 입력 칸을 비워준다.
- 중복된 값이 있을 경우 this.$emit을 통해 모달창을 띄운다.
7-5. TodoModal.vue
- src/components/common/TodoModal.vue
<!-- TodoModal.vue -->
<template>
<transition name="modal" appear>
<div class="modal modal__dim" v-on:click.self="$emit('close')">
<div class="modal__content">
<p class="modal__text">
<slot name="modal-text">(여기에 모달 내용이 들어가요)</slot>
</p>
<button class="modal__close" v-on:click="$emit('close')">OK</button>
</div>
</div>
</transition>
</template>
<script>
export default {
};
</script>
<style>
...
</style>
7-6. TodoController.vue
- Todo 목록을 정렬한다.
<!-- TodoController.vue -->
<template>
<div class="controller">
<div class="select">
<label class="blind" for="order">Order</label>
<select name="order" id="order" class="selectbox" v-model="selected" v-on:change="sortTodo">
<option value="date-asc">Oldest</option>
<option value="date-desc">Latest</option>
</select>
</div>
<button class="clear" v-on:click="clearTodo">Clear All</button>
</div>
</template>
<script>
import { mapMutations } from "vuex";
export default {
data() {
return {
selected: "date-asc"
};
},
methods: {
...mapMutations({
clearTodo: "clearAllItem"
}),
sortTodo() {
// 선택된 값에 따라 아이템 정렬
if (this.selected === "date-desc") {
this.$store.commit("sortTodoLatest");
} else if (this.selected === "date-asc") {
this.$store.commit("sortTodoOldest");
}
}
}
};
</script>
<style>
...
</style>
- 해당화면에서는 vuex와 일반 vue가 제공하는 기능을 통해 데이터를 관리한다.
- methods
- clearTodo : vuex 를 통해 전체 Todo 아이템을 지운다.(clearAllItem)
- sortTodo : vue가 제공하는 방법으로 Todo 아이템 목록을 정렬한다.(sortTodoLatest, sortTodoOlddest)
7-7. TodoHeader.vue
- 오늘 날짜를 보여준다.
<!-- TodoHeader.vue -->
<template>
<header class="header">
<h1 class="logo">
<span class="blind"></span>
</h1>
<p class="header__date">{{ timestamp }}</p>
</header>
</template>
<script>
import getDate from "../assets/common/getDate.js";
export default {
data() {
return {
timestamp: ""
};
},
created() {
this.timestamp = `${getDate().month}/${getDate().date} ${getDate().week}`;
}
};
</script>
<style>
...
</style>
- getDate.js를 통해서 날짜를 조회하여 가져온다.
8. 완성
- 모든 개발이 완성되었습니다!!!
- 모든 전체 파일을 블로그상에서는 작성하지는 못 했습니다.(예를 들어, css 같은 거...) 전체 코드는 깃허브 참고 부탁드립니다.
9. 마무리
- 공부하고 이 내용을 블로그에 작성하려고 할 때는 금방 끝나겠지 했지만 생각보다 오래 걸렸다...
- 해당 글들을 작성하면서 여러 고민도 해보고, 조금 더 이해하고 정리할 수 있었던 것 같다.
- 예전에는 jsp + spring 을 사용한 풀 스택 개발자가 많았지만, 최근 개발자 시장에서는 점점 프론트와 백엔드를 구분이 되어 개발자가 각각의 역할을 할 수 있도록 되어가고 있다.
- 작성자는 백엔드 개발자로서 길을 걸어가고 있지만, 한 번은 프론트엔드 언어를 경험해보고 이를 백엔드와 같이 사용하면서 간단하게 프로젝트를 진행하면 프론트 개발자의 입장에서도 고려하여 나중에 좋은 코드를 작성 할 수 있을 것이라고 생각하게 되어 진행하였다.
- 많은 프론트엔드 언어가 생기는 과정에서 그나마 접근이 편안 Vue.js를 선택하게 되었고, vue에 대해서는 아직 많이 부족한 부분이 많다.
참고