-
320x100
이 글은 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때문에 리액트 쓰는 사람이 있다?!)
설마 이 글을 끝까지
보고 계셨다면
넘나 감사합니다 😽
728x90'Blog > Library' 카테고리의 다른 글
[ReactJS] 2. 기능 연습 & 3. 앱 만들기 (0) 2022.02.03 [ReactJS] 1. 시작하기 (7) 2022.02.01 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (3) (0) 2020.05.30 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (2) (1) 2020.05.28 [Vue/Vuex] 뷰 실습 - Todo 웹앱 만들기 (1) (5) 2020.05.26 댓글