본문 바로가기
SPA/Vue

vue.js Todolist 만들기 [vuex 리팩토링] --3

by memeseo 2021. 5. 10.

 

Vuex로 해결할 수 있는 문제
1. MVC 패턴에서 발생하는 구조적 오류
2. 컴포넌트 간 데이터 전달 명시
3. 여러 개의 컴포넌트에서 같은 데이터를 업데이트 할 때 동기화 문제

 

vuex 라이브러리의 주요 속성인 state, getters, mutations, actions

사용하여 기존 Todolist 리팩토링을 해보자.

 

이전 게시물 보러 가기 ▼

solm-blog.tistory.com/12?category=858024

 

vue.js Todolist 만들기 [컴포넌트 통신O] --2

컴포넌트 통신하지 않은 Todolist 보러 가기 ▼ solm-blog.tistory.com/11 vue.js ToDoList 만들기 [컴포넌트 통신X] -Todolist 제작 header, input, list, footer 네 가지의 컴포넌트로 나누어 제작 -기능 설명 i..

solm-blog.tistory.com

 


1. vuex 설치

npm i vuex --save

 

2. vuex 폴더 생성

- store폴더 생성 > store.js 파일 생성

3. store.js에 vuex 등록

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

export const store = new Vuex.Store({
    // export 한 순간 store 변수를 밖에서 사용할 수 있음 
})

//vuex 등록 완료

 

4. main.js에 vuex 등록

import Vue from 'vue'
import App from './App.vue'
import {store} from './store/store'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  store,
}).$mount('#app')

 

5. 기본  사용법 (list 추가 부분 예시 첨부)

 

5-1) store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

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 : {
        // 3개의 components에서 공유되는 데이터 성질이 같아서 상위 컴포넌트에 뺐었음 이걸 store에 저장
        todoItems : storage.fetch()
    }
})

//vuex 등록 완료

 

5-2) App.vue

<template>
  <div id="app">
    <Header></Header>
    <Input v-on:addTodoItems="addOneItem"></Input>
    <List v-on:removeOneItem="removeItem"></List>
    <Footer v-on:removeAllItems="clearAllItems"></Footer>
  </div>
</template>

<script>
import Header from './components/Header'
import Input from './components/Input'
import List from './components/List'
import Footer from './components/Footer'



export default {


    methods : {
      removeItem(todoItem, index){
            localStorage.removeItem(todoItem.title);
            this.todoItems.splice(index, 1);
        },

      addOneItem(Item){
        const todoItem = {
             title : Item,
             completed : false
         }

         localStorage.setItem(todoItem.title, JSON.stringify(todoItem));
         this.$store.state.todoItems.push(todoItem); // 추가
      },

      clearAllItems(){
        localStorage.clear();
        this.todoItems = [];
      }
    },

  
  components : {
    Header,
    Input,
    List,
    Footer
  }
}
</script>

 

5-3) List.vue

<template>
  <div id="listApp">

    <!-- fontawesome CDN-->
      <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"
  />
    
    <!-- list 출력 -->
      <div v-if="this.$store.state.todoItems.length <= 0" class="ListIsEmpty"> 작성된 List가 없습니다. <br> List를 작성해주세요. </div>
      <ul v-for="(todoItem, index) in this.$store.state.todoItems" :key="todoItem.title" class="ShowTodoList">
         <li><i class="fas fa-check" @click="toggleBtn(todoItem, index)" :class="{finishToo : todoItem.completed}"></i></li>
         <li :class="{finish : todoItem.completed}">{{todoItem.title}}</li>
         <li><i class="fas fa-trash-alt" @click="OneTodoItemDel(todoItem, index)" ></i></li>
         <br>
      </ul>
      
  </div>
</template>

<script>

export default {
    methods : {
        OneTodoItemDel(todoItem, index){
           this.$emit('removeOneItem', todoItem, index);
        },

        toggleBtn(todoItem){
            todoItem.completed = !todoItem.completed;
            localStorage.removeItem(todoItem.title);
            localStorage.setItem(todoItem.title, JSON.stringify(todoItem));
        }

    }

}
</script>

 

(1) App.vue 부분 List 태그에 v-bind:propsdata="todoItems" 삭제 -- store에서 todoItems 불러올거기때문

(2) App.vue 부분 addOneItem method push부분 '

this.$store.state.todoItems.push(todoItem);' 로 변경 -- todoItems 저장 위치가 바뀌었기 때문

(3) List.vue 부분

 <ul v-for="(todoItemindexin this.$store.state.todoItems" :key="todoItem.title" class="ShowTodoList">으로 변경 이유는 위와 동일

(4) List.vue에서 props : ['propsdata'] 삭제 -- App.vue에서 데이터를 넘겨받지 않기 때문

 


최종 수정 코드

* 코드 수정이 없었던 vue는 포함하지 않았음

 

1. store.js

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex);

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 : {
        // 3개의 components에서 공유되는 데이터 성질이 같아서 상위 컴포넌트에 뺐었음 이걸 store에 저장
        todoItems : storage.fetch()
    },


    mutations : {
        addTodoItems(state, newTodoItem){
            const todoItem = {
                title : newTodoItem,
                completed : false
            }
   
            localStorage.setItem(todoItem.title, JSON.stringify(todoItem));
            state.todoItems.push(todoItem); // 추가
        },

        removeOneItem(state, payload){
            localStorage.removeItem(payload.todoItem.title);
            state.todoItems.splice(payload.index, 1);
        },

        removeAllItems(state){
            localStorage.clear();
            state.todoItems = [];
          }
    }

});

 

2. App.vue

<template>
  <div id="app">
    <Header></Header>
    <Input></Input>
    <List></List>
    <Footer></Footer>
  </div>
</template>

<script>
import Header from './components/Header'
import Input from './components/Input'
import List from './components/List'
import Footer from './components/Footer'

export default {

  components : {
    Header,
    Input,
    List,
    Footer
  }
}
</script>

 

3. Input.vue

<template>
  <div>
      <input type="text" v-model="newTodoItem" @keyup.enter="addTodoItem"/><button @click="addTodoItem">click</button>
      <modal v-if="showModal" @close="showModal=false">
          <template v-slot:header><i class="closeModal fas fa-window-close" @click="closeModalBtn"></i></template>
          <template v-slot:body>List를 입력해주세요!</template>
      </modal>
  </div>
 
</template>

<script>
import modal from './modal'

export default {
 data(){
     return {
         newTodoItem : "",
         showModal : false
     }
 },

 methods : {
     addTodoItem(){
         if(this.newTodoItem !== ""){
         //this.$emit('addTodoItems', this.newTodoItem)
         this.$store.commit('addTodoItems', this.newTodoItem); //store.js mutations를 동작시키기 위해
         this.itemClear();
         }else{
             this.showModal = !this.showModal;
         }
     },

     itemClear(){
         this.newTodoItem = "";
     },

     closeModalBtn(){
         this.showModal = !this.showModal;
     }

 },

 components : {
     modal
 }

}
</script>

 

3. List.vue

<template>
  <div id="listApp">

    <!-- fontawesome CDN-->
      <link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.8.2/css/all.min.css"
  />
    
    <!-- list 출력 -->
      <div v-if="this.$store.state.todoItems.length <= 0" class="ListIsEmpty"> 작성된 List가 없습니다. <br> List를 작성해주세요. </div>
      <ul v-for="(todoItem, index) in this.$store.state.todoItems" :key="todoItem.title" class="ShowTodoList">
         <li><i class="fas fa-check" @click="toggleBtn(todoItem, index)" :class="{finishToo : todoItem.completed}"></i></li>
         <li :class="{finish : todoItem.completed}">{{todoItem.title}}</li>
         <li><i class="fas fa-trash-alt" @click="OneTodoItemDel(todoItem, index)" ></i></li>
         <br>
      </ul>
      
  </div>
</template>

<script>

export default {
    methods : {
        OneTodoItemDel(todoItem, index){
           //this.$emit('removeOneItem', todoItem, index);
           this.$store.commit('removeOneItem', {todoItem, index});
        },

        toggleBtn(todoItem){
            todoItem.completed = !todoItem.completed;
            localStorage.removeItem(todoItem.title);
            localStorage.setItem(todoItem.title, JSON.stringify(todoItem));
        }

    }

}
</script>

 

4. Footer.vue

<template>
  <div>
    <button @click="allRomove" class="todoAllRemove"> 전체 삭제 </button>
  </div>
</template>

<script>

export default {
 methods : {
   allRomove(){
      this.$store.commit('removeAllItems');
   }
 }
  
}
</script>

 

(1) mutations는 methods랑 같은 역할을 한다고 보면 됨

(2) state 변경은 mutations를 통해서 이루어져야 하며, mutations는 commit으로 인자값을 받을 수 있음

(3) store에 인자값이 두 개 이상일 때, obj로 선언하지 말고 바로 객체로 담아서 보낼 수 있음

ex.

this.$store.commit('removeOneItem'{todoItem, index});

-store에서 객체를 받을 땐 payload로 받음

removeOneItem(statepayload){

            localStorage.removeItem(payload.todoItem.title);

            state.todoItems.splice(payload.index1);

        },

 

 

*도움 될만한 Q&A

Q. 앞서 배운데로 props와 emit를 이용하여 컴포넌트간 데이터 교환방법을 익혔는데,

Vuex의 state와 mutations로 리팩토링을 하는 것을 보면서 실무에서 props와 emit은 사용할 일이 없어보이는데,

두가지 데이터 전송방법이 어떤 경우에 각각 사용이 되고, 사용하는 것이 유리한지 궁금합니다.

 

A. 좋은 질문이네요. 제 개인적인 생각을 공유해드리자면 모든 애플리케이션 로직을 Vuex에 넣는 것은 좋지 않을 것 같습니다. Vue 창시자 에반 유도 같은 말을 한적이 있구요. Vuex가 확실히 UI 컴포넌트 구조를 바꿨을 때 기존 코드를 재활용하기 좋지만 초기에 아무래도 코드를 많이 작성해야되기 때문에 불편한 부분이 있긴 합니다. 예를 들어, 데이터를 받아오는 하나의 API만 처리하려고 해도, action - mutation - state 파일들을 다 일일이 작성해야하죠. 따라서, 해당 컴포넌트의 데이터가 다른 컴포넌트에서 사용될 일이 전혀 없다면 뷰엑스를 쓰는건 의미가 없을 것 같습니다. 참고하셔서 진행해보세요.