• [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (4)

    2020. 6. 3.

    by. 나나 (nykim)

    이 글은 Vue.js 중급 강좌 - 웹앱 제작으로 배워보는 Vue.js, ES6, Vuex 강의를 바탕으로 작성한 글입니다.
    멋진 강의 감사합니다 (__)

    시리즈: Todo 웹앱 만들기(3)

     

     

     

    8. Vuex 도입

    이번 파트에서는 Vuex를 써봅니다 👐

    이 어플리케이션은 너무너무너무 간단해서 라이브러리를 도입할 필요가 없지만, 이왕 배운 거 정리하면 좋을 거 같아서요!

     

     

    Vuex란

    Vuex에 대한 설명은 공식 문서에 멋지게 정리되어 있습니다.

     

    Vuex는 상태 관리패턴 + 라이브러리입니다. 프로젝트 내의 여러 컴포넌트를 쉽고 효율적으로 관리할 수 있도록 도와줍니다.

    공통의 state(상태, 컴포넌트 간 공유할 데이터를 말합니다)를 사용하는 컴포넌트가 많아지면 prop가 장황해지고 유지보수가 힘듭니다.

    만약 컴포넌트들이 공유하는 state를 전역으로 관리한다면, 모든 컴포넌트는 트리에 상관없이 state에 접근하거나 동작을 트리거할 수 있게 됩니다.

     

    Vuex는 Flux 패턴에서 영향을 받았으며, 이는 양방향으로 데이터가 흐르는 MVC 패턴에서 일어날 수 있는 문제점을 해결해줍니다.

    Vuex를 통해 MVC 패턴의 구조적 오류를 막을 수 있고, 여러 컴포넌트가 같은 데이터를 업데이트할 때 쉽게 동기화할 수 있습니다.

     

     

     

    Vuex의 흐름

     

    Vuex에는 중앙 건물(?)인 store가 존재합니다. 이름 그대로 '저장소'로, 애플리케이션의 state를 보관하고 있는 곳입니다.

    이 state는 아무나 막 수정할 수 없고, 오로지 변이(?!!)를 통해서만 가능합니다.

     

     

    Vuex는 단방향으로 데이터가 흐른다고 했습니다 🏊‍♀️

     

    어떤 컴포넌트에서 Dispatch로 Action을 발생시킨 경우(ex. 버튼 클릭)

    Action에서는 비동기 로직을 처리합니다. (ex. 백엔드 API를 받아옴)

     이후 Commit으로 Mutation을 호출하면,

    Mutation에서는 동기 로직을 처리합니다.

    이렇게 Mutate(변이)를 통해 State가 변경됩니다.

    그러면 Getter에 의해 컴포넌트에 데이터가 바인딩 되고 view가 뾰로롱 바뀝니다.

     

    ヽ( •́  ﹏ •̀)ノ ?????

     

     

    어... 일단 설치부터 해보죠!

     

     

     

    Vuex 설치

    npm install vuex --save

     

    CLI를 통해 Vue 프로젝트를 만들 때, Manually를 선택하면 Vuex를 추가로 설치할 수도 있습니다.

    이렇게 Vuex를 설치한 뒤, src/ 폴더 아래 store/ 폴더와 store.js 파일을 생성합니다.

     

    /* store.js */
    
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export const store = new Vuex.Store({});

     

    export했으니 main.js로 찾아가 '저 store 쓸게요!'라고 import 해줍니다.

     

    /* main.js */
    
    import Vue from 'vue'
    import App from './App.vue'
    import { store } from './store/store';
    
    new Vue({
      store,
      render: h => h(App)
    }).$mount('#app')
    

     

     


     

     

    9. Vuex 사용

     

    State

    기존에는 이렇게 데이터를 사용했습니다.

     

    <!-- Vue -->
    
    <script>
     data: {
       num : 0
     }
    </srcipt>
    
    <div> {{ num }} </div>

     

    하지만 Vuex에서는 store에 저장해 두고 사용합니다.

     

    <!-- Vuex -->
    
    <script>
     state: {
       num : 0
     }
    </srcipt>
    
    <div> {{ this.$store.state.num }} </div>

     

     

    현재 MyTodo에서는 너도나도 돌려쓰는 데이터가 있으니 바로 todoItems: [] 입니다. 이 데이터를 store의 state로 변경합니다.

     

    // store.js
    
    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export const store = new Vuex.Store({
      state: {
        todoItems: []
      }
    });

     

    그리고 기존의 리스트를 불러오던 내용도 store.js 로 가져옵니다.

     

    // 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" &&
              localStorage.key(i) !== "userName"
            ) {
              arr.push(
                JSON.parse(localStorage.getItem(localStorage.key(i)))
              );
            }
          }
        }
        return arr;
      }
    }
    
    export const store = new Vuex.Store({
      state: {
        todoItems: storage.fetch()
      }
    });

     

    이제 TodoList로 가서 기존의 props 대신 요 state 값을 받아오도록 바꿉니다.

     

    <!-- 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.$store.state.todoItems"
          v-bind:key="todoItem.item"
        >
        </li>
      </transition-group>
    </template>

     

     

     

    getters

    store 내 state를 기반으로 계산된 state가 필요할 때 사용합니다. state 변경 여부에 따라 view를 업데이트 합니다.

     

    //store.js
    
    const store = new Vuex.Store({
      state: {
        todos: [
          { id: 1, text: '...', done: true },
          { id: 2, text: '...', done: false }
        ]
      },
      getters: {
        doneTodos: state => {
          return state.todos.filter(todo => todo.done)
        }
      }
    })
    
    <p> {{ this.$store.getters.doneTodos }} </p>

     

     

     

    mutations

    뮤테이션은 state의 값을 바꿀 수 있는 유일한 방법이며, commit()으로 실행합니다.

     

    // store.js
    
    state: { myNum : 1 },
    mutations: {
      addNum(state, payload) {
        return state.myNum += payload.num;
      }
    }
    
    
    // App.vue
    
    this.$store.commit('addNum', {
      str: 'Go!',
      num: 50
    });

     

    기존의 todoItems를 state로 옮겼으니, 기존의 todoItems를 건드는 메서드들도 mutations로 옮겨야 합니다.

     

    예를 들면 새로운 할 일 목록을 추가하는 건 addOneItem()가 담당하고 있습니다.

    그런데 얘는 TodoInput 컴포넌트가 $emit으로 이벤트를 트리거하고 인자를 보내주고 있는 상태였습니다.

    따라서 기존의 $emit 대신 $commit을 바꾸고 인자도 똑같이 넘겨줍니다.

     

    // TodoInput.vue
    
    methods: {
      addTodoItem() {
        this.$store.commit("addOneItem", this.newTodoItem);
      }
    }
    // store.js
    
    mutations: {
      addTodoItem(state, todoItem) {
        // ...
        state.todoItems.push();
      }
    }

     

     

    리스트를 삭제하는 것도 이와 유사합니다.

     

    // TodoList.vue
    
    methods: {
      removeTodo() {
       this.$store.commit("removeOneItem", {
          todoItem,
          index
        });
      }
    }
    // store.js
    
    mutations: {
      removeOneItem(state, payload) {
        localStorage.removeItem(payload.todoItem.item);
        state.todoItems.splice(payload.index, 1);
      }
    }

     

     

    이런 식으로 기존의 메서드들을 하나하나 이사시켜 줍니다 🚛

     

    이렇게 mutations로 state를 변경하면 어느 컴포넌트가 접근해서 바꾼 건지 알기가 쉬워집니다.

     

     

    actions

    이 어플리케이션에선 비동기를 처리할 일이 없어서 actions를 쓸 일이 없네요 X)

    actions는 비동기 처리의 로직을 담당합니다. 아까 쓴 mutations가 동기 처리 로직을 담당하는 것과 대비되죠. 효율적인 state를 위해 서로 비동기/동기 분업해서 일하는 훈훈한 모습입니다.

    따라서 데이터 요청, Promise, async 등은 actions에서 관리합니다.

     

    // store.js
    state: {
      num: 1
    },
    mutations: {
      increaseNum(state){
        state.num ++;
      }
    },
    actions: {
      delayIncreaseNum(context){
        setTimeout( () => context.commit('increaseNum'), 1000 );
      }
    }
    
    
    // App.vue
    methods: {
      addNum(){
        this.$store.dispatch('delayIncreaseNum');
      }
    }
    //store.js
    state : {
      user: {}
    },
    mutations: {
      setData(state, fetchedData) {
        store.user = fetchedData.
      }
    },
    actions: {
      fetchUserData(context){
      	return axios.get('https://loremipsum.com/user/1')
        			.then(response => context.commit('setData', response));
      }
    };
    
    //App.vue
    methods: {
      getUser(){
        this.$store.dispatch('fetchUserData');
      }
    }

     

    이렇게 빙글빙글 도는 구조였습니다 🌪

     


     

     

    10. 헬퍼 함수

     

    헬퍼 함수란

    Vuex의 헬퍼를 사용하면 state, mutations 등을 더 빠르고 편하게 사용할 수 있습니다.

    //App.vue
    import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
    
    export default {
      computed(){
        ...mapState(['num']),
        ...mapGetters(['countedNum'])
      },
      methods: {
        ...mapMutationns(['clickBtn']),
        ...mapActions(['asyncClickBtn'])
      }
    }

     

    (객체 리터럴에서 속성 전개(...)를 쓰면, 제공된 객체의 열거형 프로퍼티를 새 객체로 복사할 수 있습니다.)

     

    아래는 mapState를 쓴 예제로, 편하게 state 값을 가져왔습니다.

     

    //App.vue
    import { mapState } from 'vuex'
    
    export default {
      computed(){
        ...mapState(['num'])
      }
    }
    
    //store.js
    state: {
      num: 10
    }
    
    <!-- <div>{{ this.$store.state.num }}</div> -->
    <div>{{ this.num }}</div>

     

     

    mapGetters를 도입하면 아래처럼 템플릿이 깔끔해집니다.

     

    <!-- 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"
        >
          <!-- ... -->
        </li>
      </transition-group>
    </template>
    
    <script>
    import { mapGetters } from "vuex";
    export default {
      computed: {
        ...mapGetters(["storedTodoItems"])
      },
      //...
    };
    </script>

     

     

    mapMutation를 도입한 모습입니다. 인자를 굳이 쓰지 않아도 자동으로 넘어가므로 편합니다.

     

    <!-- 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})"
          />
          <button class="list__delete" v-on:click="removeTodo({todoItem, index})">Delete</button>
        </li>
      </transition-group>
    </template>
    
    <script>
    import { mapGetters, mapMutations } from "vuex";
    
    export default {
      computed: {
        ...mapGetters(["storedTodoItems"]),
      },
      methods: {
        ...mapMutations({
          removeTodo: "removeOneItem",
          toggleComplete: "toggleOneItem"
        })
      }
    };
    </script>

     

    다른 함수도 예쁘게 정리해서 완성시킵니다.

     

     

     


     

     

    11. 마무리 작업

     

    모듈화

    열심히 store로 옮긴 것 좋았는데, 이렇게 하다보니 store가 복잡해져버렸네요. 중앙 창고에 이것저것 다같이 쌓여있는 느낌...?

    이는 스토어 속성을 모듈화하는 것으로 해결할 수 있습니다.

     

    getter.js mutations.js 파일을 store/ 폴더 아래 생성 후, 해당 내용들을 모듈로 분리했습니다.

     

    // getter.js
    
    const storedTodoItems = (state) => {
      return state.todoItems;
    };
    const storedName = (state) => {
      return state.userName;
    };
    const storedTodoItemsCount = (state, getters) => {
      return getters.storedTodoItems.length;
    }
    
    export { storedTodoItems, storedName, storedTodoItemsCount }; 
    // mutations.js
    
    // ...
    export { addOneItem, setUserName, ... };

     

    그리고 store에서 import 시켜주면 깔끔하게 정리정돈 완료!

     

    import Vue from 'vue'
    import Vuex from 'vuex'
    import storage from "./modules/storage";
    import * as getters from "./modules/getters";
    import * as mutations from "./modules/mutations";
    
    Vue.use(Vuex);
    
    export const store = new Vuex.Store({
      state: {
        todoItems: storage.fetch(),
        userName: storage.fetchName(),
      },
      getters: getters,
      mutations: mutations
    });

     

     

    만약 하나의 store로 관리하기 힘들 만큼 앱이 비대해지면 store자체를 모듈화하기도 합니다.

    하지만 저는 내용이 그리 많지 않으므로 여기까지만 정리했습니다.

     

    이제 리팩토링, 버그 픽스까지 마치면 작업이 끝납니다.

     

     

     

    Git 배포

    # 배포 파일 생성
    `$ npm run build`
    
    # gh-pages 브랜치 생성
    `$ git checkout -b gh-pages`
    
    
    # vue.config.js내 경로 지정
    ```
    module.exports = {
      publicPath: '{저장소 이름과 동일}'
    }
    ```
    
    # dist/ 푸쉬 (.gitignore에서 주석 해제 필요)
    `$ git add dist && git commit -m "Initial dist subtree commit"`
    `$ git subtree push --prefix dist origin gh-pages`

     

    참고 글을 바탕으로 작업했습니다. 이 부분도 나중에 따로 정리하고 싶네요!

    처음 배포 시에 Failed to load resource: the server responded with a status of 404 (Not Found) 오류가 팡팡 터졌었는데 이유는 package.json의 name과 git 저장소명이 일치하지 않아서였습니다 허허허

     

     


     

     

    12. 완성!

     

     

    https://nana-like.github.io/vue-mytodo/

     

     

    프로젝트의 목적

    간단하지만 수많은 삽질로 쌓아올린 결과물을 보니 뿌듯하네요!

    이 프로젝트를 시작한 소기의 목적을 달성할 수 있어서 무척 좋았습니다.

     

    목적1: 프레임워크 경험해보기

    "프레임워크 써본 적 있어요?" "아뇨(쭈굴)" 하던 걸 벗어나고 싶어서 호기롭게 도전한 것도 있습니다ㅋㅋㅋㅋ 

     

    목적2: FE가 어떻게 일하는지 알기 & 협업을 위한 좋은 퍼블리싱 방향 찾기

    작업을 할 때마다 '내가 어떻게 해야 다음 개발자가 작업하기 편할까?'하는 고민이 많았습니다.

    어떤 마크업이 편하면서도 아름다울지 고민해 볼 수 있어서 좋았어요!

     

    목적3: 자바스크립트 익숙해지기

    "앗 분명 책을 보고 있는데 왜 계속 같은 페이지지! 이해가 안 되니 노잼..." -> "뭔가 만들면서 하다보면 재미가 붙지 않을까?"

    역시 사람은 뭔가 만들고 부수고 삽질하는 과정이 필요한 것 같아요 하하하ㅎㅎ

     

     

    느낀 점

    재밌어요! 이해는 둘째치고 제이쿼리 처음 접했을 때 그 느낌!

    ".hide()하면 사라져요! 와! 근데 왜 사라지죠?"

    일단 숨겼다는 게 중요한 게 아닐까요. 재미를 붙이는 게 중요한 거죠 🙈

     

    리액트도 궁금해요

    뷰의 느낌적인 느낌을 맛봤으니 리액트는 어떤 느낌일지 궁금합니다.

    예전에 스타일드 컴포넌트를 접하고 '이것이 차세대 CSS인가..!' 싶은 기억이 있어서, 이걸 꼭 한 번 써보고 싶네요. (CSS때문에 리액트 쓰는 사람이 있다?!)

     

     

     

     

     

     

    설마 이 글을 끝까지

    보고 계셨다면

     

     

    넘나 감사합니다 😽

    댓글 0