-
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.jsimport Vue from 'vue'import Vuex from 'vuex'Vue.use(Vuex)export const store = new Vuex.Store({state: {todoItems: []}});그리고 기존의 리스트를 불러오던 내용도 store.js 로 가져옵니다.
// store.jsimport 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"><liclass="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.jsconst 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.jsstate: { myNum : 1 },mutations: {addNum(state, payload) {return state.myNum += payload.num;}}// App.vuethis.$store.commit('addNum', {str: 'Go!',num: 50});기존의 todoItems를 state로 옮겼으니, 기존의 todoItems를 건드는 메서드들도 mutations로 옮겨야 합니다.
예를 들면 새로운 할 일 목록을 추가하는 건 addOneItem()가 담당하고 있습니다.
그런데 얘는 TodoInput 컴포넌트가 $emit으로 이벤트를 트리거하고 인자를 보내주고 있는 상태였습니다.
따라서 기존의 $emit 대신 $commit을 바꾸고 인자도 똑같이 넘겨줍니다.
// TodoInput.vuemethods: {addTodoItem() {this.$store.commit("addOneItem", this.newTodoItem);}}// store.jsmutations: {addTodoItem(state, todoItem) {// ...state.todoItems.push();}}리스트를 삭제하는 것도 이와 유사합니다.
// TodoList.vuemethods: {removeTodo() {this.$store.commit("removeOneItem", {todoItem,index});}}// store.jsmutations: {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.jsstate: {num: 1},mutations: {increaseNum(state){state.num ++;}},actions: {delayIncreaseNum(context){setTimeout( () => context.commit('increaseNum'), 1000 );}}// App.vuemethods: {addNum(){this.$store.dispatch('delayIncreaseNum');}}//store.jsstate : {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.vuemethods: {getUser(){this.$store.dispatch('fetchUserData');}}이렇게 빙글빙글 도는 구조였습니다 🌪
10. 헬퍼 함수
헬퍼 함수란
Vuex의 헬퍼를 사용하면 state, mutations 등을 더 빠르고 편하게 사용할 수 있습니다.
//App.vueimport { mapState, mapGetters, mapMutations, mapActions } from 'vuex'export default {computed(){...mapState(['num']),...mapGetters(['countedNum'])},methods: {...mapMutationns(['clickBtn']),...mapActions(['asyncClickBtn'])}}(객체 리터럴에서 속성 전개(
...
)를 쓰면, 제공된 객체의 열거형 프로퍼티를 새 객체로 복사할 수 있습니다.)아래는 mapState를 쓴 예제로, 편하게 state 값을 가져왔습니다.
//App.vueimport { mapState } from 'vuex'export default {computed(){...mapState(['num'])}}//store.jsstate: {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"><liclass="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"><liclass="list__item"v-for="(todoItem, index) in this.storedTodoItems"v-bind:key="todoItem.item"><inputtype="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.jsconst 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나나 (nykim)쉽고, 재밌고, 특별한 걸 좋아해요. 걷고 뛰고 구르면서 나아가는 중.
'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