Vuex를 사용한 Todo프로젝트 만들기
구현 기능 : Todo CRUD, Todo 개수 계산 (전체, 완료, 미완료)
vue create todo-vuex-app
cd todo-vuex-app
vue add vuex
프로젝트 생성, vuex plugin
컴포넌트 구성
App - TodoForm, TodoList(TodoListItem)
위와 같은 구조로 컴포넌트 작성한다.
<template>
<div>Todo</div>
</template>
<script>
export default {
name :'TodoListItem'
}
</script>
<style>
</style>
components/TodoListItem.vue
<template>
<div>
<TodoListItem/>
</div>
</template>
<script>
import TodoListItem from '@/components/TodoListItem.vue'
export default {
name : 'TodoList',
components : {
TodoListItem,
}
}
</script>
<style>
</style>
components/TodoList.vue
<template>
<div>Todo Form</div>
</template>
<script>
export default {
name : 'TodoForm'
}
</script>
<style>
</style>
components/TodoForm.vue
<template>
<div id="app">
<h1>Todo List</h1>
<TodoList/>
<TodoForm/>
</div>
</template>
<script>
import TodoList from '@/components/TodoList.vue'
import TodoForm from '@/components/TodoForm.vue'
export default {
name: 'App',
components: {
TodoList,
TodoForm,
}
}
</script>
App.vue
컴포넌트 등록된 화면 구현까지 했다.
state
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos : [
{
title : '할 일1',
isCompleted : false,
},
{
title : '할 일 2',
isCompleted : false,
}
]
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
}
})
store - index.js
Vue 개발자 도구에서 state 데이터를 확인할 수 있다.
<template>
<div>
<TodoListItem
v-for ="(todo, index) in todos"
:key="index"/>
</div>
</template>
<script>
import TodoListItem from '@/components/TodoListItem.vue'
export default {
name : 'TodoList',
components : {
TodoListItem,
},
computed : {
todos() {
return this.$store.state.todos
}
}
}
</script>
<style>
</style>
TodoList.vue에서 state데이터를 가져오도록 한다.
computed에 Vue Store의 state에 접근하여 todos를 가져온다.
<template>
<div>
<TodoListItem
v-for ="(todo, index) in todos"
:key="index"
:todo = "todo"
/>
</div>
</template>
TodoList.vue에서 TodoListItem.vue로 todo를 보내주기 위해 속성값을 지정해준다.
<template>
<div>{{todo.title}} </div>
</template>
<script>
export default {
name :'TodoListItem',
props: {
todo : Object,
}
}
</script>
TodoListItem.vue
props로 내려받은 데이터 todo를 등록하고, {{todo.title}}으로 사용했다.
출력 확인
createTodo
<template>
<div>
<input
type="text"
v-model="todoTitle"
@keyup.enter = "createTodo"
>
</div>
</template>
<script>
export default {
name : 'TodoForm',
data(){
return {
todoTitle: null,
}
},
methods : {
createTodo(){
console.log(this.todoTitle)
}
}
}
</script>
TodoForm.vue
todoTitle 데이터를 만들고, 이것을 입력받을 input 태그를 생성했다.
input 태그를 v-model로 데이터와 연결하여 양방향 바인딩한다.
enter이벤트를 사용해서 createTodo메서드를 실행한다.
입력하여 엔터치니, 콘솔에 출력되는 것을 확인하였다.
메서드 실행을 확인한것이다.
<script>
export default {
name : 'TodoForm',
data(){
return {
todoTitle: null,
}
},
methods : {
createTodo(){
// console.log(this.todoTitle)
this.$store.dispatch('createTodo',this.todoTitle)
this.todoTitle = null
}
}
}
</script>
원하는 작업은 actions를 호출하는 것이기 때문에 dispatch를 사용한다.
this.$store.dispatch()로 createTodo action을 호출하고 todoTitle을 함께 전달한다.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos : [
{
title : '할 일1',
isCompleted : false,
},
{
title : '할 일 2',
isCompleted : false,
}
]
},
getters: {
},
mutations: {
},
actions: {
createTodo(context,todoTitle){
const todoItem = {
title : todoTitle,
isCompleted : false,
}
console.log(todoItem)
}
},
modules: {
}
})
actions를 추가했다.
method에서 dispatch로 불러왔던 actions의 이름을 지정하고 인자는 context, 받아온데이터(todoTitle)이다.
actions에는 보통 비동기 관련 작업이 진행되지만 현재 별도의 비동기 관련 작업이 불필요하기때문에 입력받은 제목을 todoItem으로 만드는 과정을 actions에서 작성했다.
createTodo(컴포넌트의 메서드)에서 보낸 데이터를 createTodo(action 메서드)에서 수신후 todoItem object를 생성했다.
할 일 3 을 입력하고 엔터쳤을 때,
actions에서 todoItem을 만들어 콘솔에 출력하였다.
actions: {
createTodo(context,todoTitle){
const todoItem = {
title : todoTitle,
isCompleted : false,
}
// console.log(todoItem)
context.commit('CREATE_TODO',todoItem)
}
},
context.commit()으로 CREATE_TODO mutations 메서드에 todoItem을 전달하며 호출했다.
mutations: {
CREATE_TODO(state, todoItem){
state.todos.push(todoItem)
}
},
mutations에서 CREATE_TODO라는 mutation을 만들었다.
첫번째 인자는 state, 두번째 인자는 받아온 데이터인 todoItem이다.
state.todos에 접근해서 배열에 요소를 (todoItem을) push()로 추가하였다.
input 에 todo 작성 후 엔터하면 목록에 추가되어 보여진다.
state: {
todos : []
},
예시를 위해 미리 추가했던 todos를 삭제하고 빈 배열로 수정한다.
빈 문자열 제외
<template>
<div>
<input
type="text"
v-model.trim="todoTitle"
@keyup.enter = "createTodo"
>
</div>
</template>
<script>
export default {
name : 'TodoForm',
data(){
return {
todoTitle: null,
}
},
methods : {
createTodo(){
if (this.todoTitle){
this.$store.dispatch('createTodo',this.todoTitle)
}
this.todoTitle = null
}
}
}
</script>
TodoForm.vue
공백 문자가 입력되지 않도록 v-model.trim으로 좌우 공백을 삭제했고,
if(this.todoTitle)으로 빈문자열이 아닌 경우에만 작성하도록 했다.
- vue 컴포넌트의 method에서 dispatch()를 사용해 actions 메서드 호출
- actions에 정의된 함수는 commit()을 사용해 mutations 호출
- mutations에 정의된 함수가 최종적으로 state를 변경
deleteTodo
<template>
<div>
<span>{{ todo.title }} </span>
<button @click="deleteTodo">Delete</button>
</div>
</template>
<script>
export default {
name: "TodoListItem",
props: {
todo: Object,
},
methods: {
deleteTodo() {
this.$store.dispatch("deleteTodo", this.todo);
},
},
};
</script>
TodoListItem.vue
click하면 deleteTodo 메서드 실행,
삭제해야 하는 todo데이터를 가지고 deleteTodo action메서드 호출한다.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos : []
},
getters: {
},
mutations: {
CREATE_TODO(state, todoItem){
state.todos.push(todoItem)
},
DELETE_TODO(state,todoItem){
const index = state.todos.indexOf(todoItem)
state.todos.splice(index,1)
}
},
actions: {
createTodo(context,todoTitle){
const todoItem = {
title : todoTitle,
isCompleted : false,
}
context.commit('CREATE_TODO',todoItem)
},
deleteTodo(context, todoItem){
context.commit('DELETE_TODO',todoItem)
},
},
modules: {
}
})
deleteTodo action 메서드와 DELETE_TODO mutations를 만들었다.
todoItem을 가지고 deleteTodo가 실행된다.
첫번째 인자 context.commit으로 mutations을 실행한다. 이때 todoItem도 같이 보내준다.
DELETE_TODO mutations
indexOf로 todo의 인덱스를 찾고,
state.todos.splice(index,1) 으로 state.todos를 변경한다.
인덱스 index인 요소부터 1개의 요소를 삭제하여 원본에 반영한다는 동작이다.
updateTodoStatus
<template>
<div>
<span @click = "updateTodoStatus">{{ todo.title }} </span>
<button @click="deleteTodo">Delete</button>
</div>
</template>
<script>
export default {
name: "TodoListItem",
props: {
todo: Object,
},
methods: {
deleteTodo() {
this.$store.dispatch("deleteTodo", this.todo);
},
updateTodoStatus(){
this.$store.dispatch('updateTodoStatus',this.todo)
}
},
};
</script>
TodoListItem.vue
todo를 클릭하면 상태를 변경하도록 한다. (isCompletd값 토글하기)
해당 todo를 가지고 메서드를 실행하여 관련 actions 메서드를 호출한다.
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
todos : []
},
getters: {
},
mutations: {
CREATE_TODO(state, todoItem){
state.todos.push(todoItem)
},
DELETE_TODO(state,todoItem){
const index = state.todos.indexOf(todoItem)
state.todos.splice(index,1)
},
UPDATE_TODO_STATUS(state, todoItem){
state.todos = state.todos.map((todo) => {
if (todo === todoItem){
todo.isCompleted = !todo.isCompleted
}
return todo
})
}
},
actions: {
createTodo(context,todoTitle){
const todoItem = {
title : todoTitle,
isCompleted : false,
}
context.commit('CREATE_TODO',todoItem)
},
deleteTodo(context, todoItem){
context.commit('DELETE_TODO',todoItem)
},
updateTodoStatus(context,todoItem){
context.commit('UPDATE_TODO_STATUS',todoItem)
}
},
modules: {
}
})
actions에 updateTodoStatus가 todoItem을 받아오며 호출되었다.
context.commit()으로 todoItem을 보내면서 mutations을 호출했다.
UPDATE_TODO_STATUS mutations 메서드는 state.todos를 map으로 각 요소를 todoItem과 비교하여 같으면 isCompleted를 바꿔주었다.
isCompleted를 반대로 변경 후 기존 배열을 업데이트했다.
UPDATE_TODO_STATUS(state, todoItem){
const index = state.todos.indexOf(todoItem)
state.todos[index].isCompleted = !state.todos[index].isCompleted
}
map 대신 index를 이용할 수도 있다.
todo 를 클릭하면 isCompleted 값이 바뀌는 것을 확인했다.
<template>
<div>
<span @click = "updateTodoStatus"
:class = "{ 'is-completed' : todo.isCompleted }">
{{ todo.title }} </span>
<button @click="deleteTodo">Delete</button>
</div>
</template>
<style>
.is-completed{
text-decoration: line-through;
}
</style>
is-completed 스타일 클래스를 지정했다.
isCompleted 값에 따라 클래스가 적용될 수 있도록 :class로 연결한다.
클래스가 토글 방식으로 적용된다.
allTodosCount
getters: {
allTodosCount(state){
return state.todos.length
}
},
state에 있는 todos 배열 길이를 계산하기 위해 allTodosCount getters를 작성했다.
<template>
<div id="app">
<h1>Todo List</h1>
<h2> All Todos : {{allTodosCount}}</h2>
<TodoList/>
<TodoForm/>
</div>
</template>
<script>
import TodoList from '@/components/TodoList.vue'
import TodoForm from '@/components/TodoForm.vue'
export default {
name: 'App',
components: {
TodoList,
TodoForm,
},
computed : {
allTodosCount(){
return this.$store.getters.allTodosCount
}
}
}
</script>
App.vue
getters에 계산된 값을 각 컴포넌트의 computed에서 사용한다.
completedTodosCount
getters: {
allTodosCount(state){
return state.todos.length
},
completedTodosCount(state){
const completedTodos = state.todos.filter((todo)=>{
return todo.isCompleted==true
})
return completedTodos.length
}
},
완료된 todo 개수를 만들기 위해 getters에 새로운 completedTodosCount를 작성했다.
isCompleted가 true인 todo들만 필터링한 배열을 만들고 길이를 계산한다.
<template>
<div id="app">
<h1>Todo List</h1>
<h2> All Todos : {{allTodosCount}}</h2>
<h2> Completed Todo : {{completedTodosCount}}</h2>
<TodoList/>
<TodoForm/>
</div>
</template>
<script>
import TodoList from '@/components/TodoList.vue'
import TodoForm from '@/components/TodoForm.vue'
export default {
name: 'App',
components: {
TodoList,
TodoForm,
},
computed : {
allTodosCount(){
return this.$store.getters.allTodosCount
},
completedTodosCount(){
return this.$store.getters.completedTodosCount
}
}
}
</script>
App.vue
getters를 이용한 computed를 등록하고 사용했다.
unCompletedTodosCount
getters: {
allTodosCount(state){
return state.todos.length
},
completedTodosCount(state){
const completedTodos = state.todos.filter((todo)=>{
return todo.isCompleted==true
})
return completedTodos.length
},
unCompletedTodosCount(state,getters){
return getters.allTodosCount - getters.completedTodosCount
}
},
미완료한 todo 개수를 만들기 위해 기존에 만들었던 getter를 사용하는 getter를 만들었다.
getter의 두번째 인자는 getters으로 받는 것을 활용했다.
전체개수 - 완료개수로 return 했다.
<template>
<div id="app">
<h1>Todo List</h1>
<h2> All Todos : {{allTodosCount}}</h2>
<h2> Completed Todo : {{completedTodosCount}}</h2>
<h2> unCompleted Todo : {{unCompletedTodosCount}}</h2>
<TodoList/>
<TodoForm/>
</div>
</template>
<script>
import TodoList from '@/components/TodoList.vue'
import TodoForm from '@/components/TodoForm.vue'
export default {
name: 'App',
components: {
TodoList,
TodoForm,
},
computed : {
allTodosCount(){
return this.$store.getters.allTodosCount
},
completedTodosCount(){
return this.$store.getters.completedTodosCount
},
unCompletedTodosCount(){
return this.$store.getters.unCompletedTodosCount
}
}
}
</script>
App.vue
getters에서 가져와 computed에 등록하고, 사용했다.
todo list 완성!
Local Storage
완성했지만, 브라우저가 종료되면 (새로고침) 데이터가 사라진다.
브라우저의 local storage에 todo 데이터를 저장하여 브라우저를 종료하고 다시 실행해도 데이터가 보존될 수 있도록 해야 한다.
Window.localStorage
브라우저에서 제공하는 저장공간 중 하나인 Local Storage에 관련 된 속성
만료되지 않고 브라우저를 종료하고 다시 실행해도 데이터가 보존 된다.
데이터가 문자열 형태로 저장된다.
setItem(key, value) : key, value 형태로 데이터 저장
getItem(key) : key에 해당하는 데이터 조회
todos 배열을 Local Storage에 저장하기
actions: {
saveTodosToLocalStorage(context){
const jsonTodos = JSON.stringify(context.state.todos)
localStorage.setItem('todos',jsonTodos)
}
},
state를 변경하는 작업이 아니기때문에 mutations가 아닌 actions에 작성한다.
데이터가 문자열 형태로 저장되어야 하기 때문에 JSON.stringify를 사용해 문자열로 변환해주는 과정이 필요하다.
context.state.todos를 문자열로 변환하여 변수에 담고, 그것을 setItem으로 저장한다.
localStorage.setItem(key, value)
actions: {
createTodo(context,todoTitle){
const todoItem = {
title : todoTitle,
isCompleted : false,
}
context.commit('CREATE_TODO',todoItem)
context.dispatch('saveTodosToLocalStorage')
},
deleteTodo(context, todoItem){
context.commit('DELETE_TODO',todoItem)
context.dispatch('saveTodosToLocalStorage')
},
updateTodoStatus(context,todoItem){
context.commit('UPDATE_TODO_STATUS',todoItem)
context.dispatch('saveTodosToLocalStorage')
},
saveTodosToLocalStorage(context){
const jsonTodos = JSON.stringify(context.state.todos)
localStorage.setItem('todos',jsonTodos)
}
},
todo 생성, 삭제, 수정시에 모두 saveTodosToLocalStorage action 메서드가 실행되도록 한다.
개발자도구 - application - storage - local Storage에서 todos 가 변경될 때 마다 저장되는 것을 확인할 수 있다.
저장된 데이터를 가져오기
1. 불러오기 버튼 작성
<template>
<div id="app">
<h1>Todo List</h1>
<h2> All Todos : {{allTodosCount}}</h2>
<h2> Completed Todo : {{completedTodosCount}}</h2>
<h2> unCompleted Todo : {{unCompletedTodosCount}}</h2>
<TodoList/>
<TodoForm/>
<button @click="loadTodos">Todo 불러오기</button>
</div>
</template>
App.vue
@click했을 때 loadTodos 메서드 실행하도록 하는 버튼을 만든다.
2. loadTodos 메서드 작성
methods : {
loadTodos() {
this.$store.dispatch('loadTodos')
}
}
App.vue
3. loadTodos action 메서드 작성
actions: {
loadTodos(context){
context.commit('LOAD_TODOS')
}
},
index.js
state.todos를 변경해야하기 때문에 mutations를 호출한다.
4. LOAD_TODOS mutation 메서드 작성
mutations: {
LOAD_TODOS(state){
const localStorageTodos = localStorage.getItem('todos')
const parsedTodos = JSON.parse(localStorageTodos)
state.todos = parsedTodos
}
},
localStorage.getItem()으로 localStorage에 저장했던 todos(key값) 데이터를 가져온다.
문자열 데이터를 다시 object 타입으로 변환해야 하기 때문에 JSON.parse() 한다.
state.todos에 변환한 데이터를 대입한다.
새로고침하면 todo 데이터가 없지만, 불러오기 버튼을 누르면 이전까지 작성한 정보를 가져온다.
vuex-persistedstate
Vuex state를 자동으로 브라우저의 Local Storage에 저장해주는 라이브러리 중 하나
페이지가 새로고침 되어도 Vuex state를 유지시킨다.
Local Staorge에 저장된 data를 자동으로 state로 불러온다.
npm i vuex-persistedstate
설치
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export default new Vuex.Store({
plugins : [
createPersistedState(),
],
import하고 plugins에 등록한다.
이전까지 작성했던 localstorage 관련 코드를 모두 삭제해도 된다.
불러오기 버튼 없이 자동으로 데이터를 불러오게 된다.
'Front-end > Vue.js' 카테고리의 다른 글
Routing , Vue Routing 시작하기 (0) | 2022.11.09 |
---|---|
UX & UI / Prototyping (0) | 2022.11.09 |
Lifecycle Hooks - created, mounted, updated, destroyed (0) | 2022.11.07 |
Vuex state management - store : state, actions, mutations, getters (0) | 2022.11.07 |
vue - youtube api (0) | 2022.11.03 |