vuex

Vuex는 Vue.js 애플리케이션에 대한 상태 관리 패턴 + 라이브러리 입니다. 애플리케이션의 모든 컴포넌트에 대한 중앙 집중식 저장소 역할을 하며 예측 가능한 방식으로 상태를 변경할 수 있습니다.

공통의 상태를 공유하는 여러 컴포넌트 가 있는 경우 컴포넌트에서 공유된 상태를 추출하고 이를 전역 싱글톤으로 관리해야 합니다. 이를 통해 우리의 컴포넌트 트리는 커다란 "뷰"가 되며 모든 컴포넌트는 트리에 상관없이 상태에 액세스하거나 동작을 트리거 할 수 있습니다!

Flux

  • MVC 패턴의 복잡한 데이터 흐름 문제를 해결하는 개발 패턴 - Unidirectional data flow
  1. action : 화면에서 발생하는 이벤트 또는 사용자의 입력
  2. dispatcher : 데이터를 변경하는 방법, 메서드
  3. model : 화면에 표시할 데이터
  4. view : 사용자에게 비춰지는 화면

MVC 패턴과 Flux 패턴 비교

  1. MVC 패턴 Controller -> Model <-> View
  • 기능 추가 및 변경에 따라 생기는 문제점을 예측할 수가 없음.
  • 앱이 복잡해지면서 생기는 업데이트 루프
  1. Flux 패턴 Action -> Dispatcher -> Model -> View
  • 데이터의 흐름이 여러 갈래로 나뉘지 않고 단방향으로만 처리 Flux 패턴

Vuex 컨셉

  • State : 컴포넌트 간에 공유하는 데이터 data()
  • View : 데이터를 표시하는 화면 template
  • Action : 사용자의 입력에 따라 데이터를 변경하는 methods vuex 도표

Vuex 구조

컴포넌트 -> 비동기 로직 -> 동기 로직 -> 상태 vuex 도표2

설치

<script src="https://unpkg.com/vuex"></script>

vuex는 싱글 파일 컴포넌트 체계에서 npm 방식으로 라이브러리를 설치하는게 좋다.

npm install vuex --save
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

모듈 시스템과 함께 사용하면 Vue.use()를 통해 Vuex를 명시적으로 추가해야 합니다. 전역 스크립트 태그를 사용할 때는 이 작업을 할 필요가 없습니다.

vuex 기술 요소

  • state : 여러 컴포넌트에 공유되는 데이터 data
  • getters : 연산된 state 값을 접근하는 속성 computed
  • mutation : state 값을 변경하는 이벤트 로직 메서드 methods
  • actions : 비동기 처리 로직을 선언하는 메서드 async methods

actions 비동기 코드 예제

/* store.js */
mutations: {
  setData(state, fetchedData){
    state.product = fetchedData;
  }
},
actions:{
  fetchProductData(context){
    return axios.get('https://domain.com/products/1')
                .then(response => context.commit('setData', response));
  }
}
/* App.vue */
methods:{
  getProduct(){
    this.$store.dispatch('fetchProductData');
  }
}

Store 속성들을 더 쉽게 사용하는 방법

  • state -> mapState
  • getters -> mapGetters
  • mutations -> mapMutations
  • actions -> mapActions

헬퍼의 사용법

  • 헬퍼를 사용하고자 하는 vue 파일에서 아래와 같이 해당 헬퍼를 로딩
/* App.vue */
import { mapState } from 'vuex'
import { mapGetters } from 'vuex'
import { mapMutations } from 'vuex'
import { mapActions } from 'vuex'

export default{
  computed(){
    ...mapState(['num']),
    ...mapGetters(['countedNum'])
  },
  methods:{
    ...mapMutations(['clickBtn']),
    ...mapActions(['asyncClickBtn'])
  }
}

mapState

/* App.vue */
<!-- <p>{{ this.$store.state.num }}</p> -->
<p>{{ this.num }}</p>

import { mapState } from 'vuex'
computed(){
  ...mapState(['num'])
  // num() { return this.$store.state.num; }
}
/* store.js */
state:{
  num:10
}

mapGetters

/* App.vue */
<!-- <p>{{ this.$store.getters.reverseMessage }}</p> -->
<p>{{ this.reverseMessage }}</p>

import { mapGetters } from 'vuex'
computed(){
  ...mapGetters(['reverseMessage'])  
}
/* store.js */
getters:{
  reverseMessage(state){
    return state.msg.split('').reverse().jogin('');
  }
}

mapMutations

/* App.vue */
<button @click="clickBtn">popup message</button>

import { mapMutaions } from 'vuex'
methods: {
  ...mapMutations(['clickBtn'])  ,
  authLogin(){},
  displayTable(){}
}
/* store.js */
mutations:{
  clickBtn(state){
    alert(state.msg);
  }
}

mapActions

/* App.vue */
<button @click="delayClickBtn">delay popup message</button>

import { mapActions } from 'vuex'
methods: {
  ...mapActions([
    'delayClickBtn', // 'delayClickBtn' : delayClickBtn
    ]) 
}
/* store.js */
actions:{
  delayClickBtn(context){
    setTimeout(() => context.commit('clickBtn'), 2000);
  }
}

시작하기

Vuex Getting Started

# vue cli 프로젝트 
vue create vuex-start
cd vuex-start
npm install vuex
/* src/main.js */
import Vue from 'vue'
import Vuex from 'vuex';
import App from './App.vue'

Vue.use(Vuex);

const store = new Vuex.Store({
  state:{
    count:0
  },
  mutations: {
  	increment: state => state.count++,
    decrement: state => state.count--
  }
});

new Vue({
  render: h => h(App),
  store,  
}).$mount('#app')
/* src/App.vue */
<template>
  <div id="app">
  <p>{{ count }}</p>
  <p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
  </p>
</div>
</template>

<script>
export default {
  name: 'app',
  computed: {
    count () {
      return this.$store.state.count
    }
  },
  methods: {
    increment () {
      this.$store.commit('increment')
    },
    decrement () {
    	this.$store.commit('decrement')
    }
  }
}
</script>

<style>
</style>

예제1

/* main.js */
import Vue from 'vue'
import App from './App.vue'
import Vuex from 'vuex';

Vue.config.productionTip = false

Vue.use(Vuex);

let store = new Vuex.Store({
  // data 속성
  state: {
    id: '...'
  },
  // methods 속성
  mutations: {
    changeId(state, id) {
      state.id = id;
    }
  }
})

new Vue({
  store,
  render: h => h(App),
}).$mount('#app')
/* App.vue */
<template>
  <div>
    <app-header></app-header>
    <app-content></app-content>
  </div>
</template>

<script>
import AppHeader from './components/AppHeader.vue';
import AppContent from './components/AppContent.vue';
export default {
  components: {
    AppHeader,
    AppContent,
  },
}
</script>
/* components/AppHeader.vue */
<template>
  <div>
    <span>login as {{ this.$store.state.id }}</span>
  </div>
</template>
/* components/AppContent.vue */
<template>
  <div>
    <login-form></login-form>
  </div>
</template>

<script>
import LoginForm from './LoginForm.vue';
export default {
  components: {
    LoginForm,
  }
}
</script>
/* components/LoginForm.vue */
<template>
  <div>
    <form>
      <div>
        <label for="username">ID : </label>
        <input id="username" type="text" v-model="id">
      </div>
      <div>
        <label for="password">PW : </label>
        <input id="password" type="password" v-model="pw">
      </div>
      <button v-on:click.prevent="loginUser">login</button>
    </form>    
  </div>
</template>

<script>
export default {
  data() {
    return {
      id: '',
      pw: '',
      logMessage: '',
    }
  },
  methods: {
    loginUser() {
      const loginId = this.id;
      this.$store.commit('changeId', loginId);
    }
  }
}
</script>

예제2

/* main.js */
import Vue from 'vue'
import App from './App.vue'
import App2 from './App2.vue'
import { store } from './store/index.js';

new Vue({
  el: '#app',
  store,
  render: h => h(App)
})
/* App.vue */
<template>
  <div>
    <span v-bind:class="errorClass">{{ reversedMessage }}</span>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'hi',
      successClass: 'success',
      isError: true,
    }
  },
  computed: {
    reversedMessage() {
      return this.message + '!!';
    },
    errorClass() {
      if (this.isError) {
        return 'error';
      } else {
        return 'success';
      }
    }
  }
}
</script>

<style scoped>
.error {
  color: red;
}
.success {
  color:  blue;
}
</style>
/* App2.vue */
<template>
  <div>
    {{ this.$store.state.user }}
    <button v-on:click="getUser">get user</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: ''
    }
  },
  methods: {
    getUser() {
      this.$store.dispatch('FETCH_USER');     
    }
  },
}
</script>
/* store/index.js */
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

export const store = new Vuex.Store({
  // data
  state: {
    msg: 'hi',
    user: {},
  },
  // computed
  getters: {
    exclaimMessage(state) {
      return state.msg + '!!';
    }
  },
  mutations: {
    setUser(state, user) {
      state.user = user;
    }
  },
  // methods
  actions: {
    FETCH_USER(context) {
      fetch('https://jsonplaceholder.typicode.com/users/1')
        .then(response => response.json())
        .then(json => {
          context.commit('setUser', json);
        })
        .catch(error => console.log(error));
    }
  }
});

예제3

vue create vuex-token
cd vuex-token
npm install vuex
npm install vue-router
npm install axios
/* src/main.js */
import Vue from 'vue'
import App from './App.vue'
import { router } from './routes/index.js';
import store from './store/index.js';

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  router,
  store,
}).$mount('#app')
/* src/App.vue */
<template>
  <div>
    <div>
      <router-link to="/login">login view</router-link>
      <router-link to="/main">main view</router-link>
    </div>    
    <router-view></router-view>
  </div>
</template>

<script>
export default {
  /* created(){
    this.$router.push('/login');
  } */
}
</script>

<style scoped>
  a{margin:0.3rem;}
</style>
/* src/routes/index.js */
import Vue from 'vue';
import VueRouter from 'vue-router';
import LoginView from '../views/LoginView.vue';
import MainView from '../views/MainView.vue';
import store from '../store/index.js';

Vue.use(VueRouter);

export const router = new VueRouter({
    routes: [
        {
            path:'/',
            redirect:'/login',
        },
        {
            path: '/login',
            component: LoginView,
        },
        {
            path: '/main',
            component: MainView,
            beforeEnter: function(to, from, next){
                if(store.state.token !== ''){
                    next();
                }else{
                    alert('login');
                }                
            }
        }
    ]
})
/* src/store/index.js*/
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex); // 플러그인 셋업

export default new Vuex.Store({
    state:{
        token:''
    },
    mutations:{
        setToken(state, token){
            state.token = token;
        }
    }
})
/* src/views/LoginView.vue*/
<template>
    <div>
        <login-form></login-form>
    </div>
</template>

<script>
import LoginForm from '../components/LoginForm.vue';
export default {
    components:{
        LoginForm,
    }
}
</script>

<style>

</style>
/* src/views/MainView.vue*/
<template>
    <div>
        main
    </div>
</template>

<script>
export default {

}
</script>

<style>

</style>
/* src/components/LoginForm.vue */
<template>
  <div>
    <form> 
      <div>
        <label for="username">ID : </label>
        <input id="username" type="text" v-model="username">
      </div>
      <div>
        <label for="password">PW : </label>
        <input id="password" type="password" v-model="password">
      </div>
      <div>
        <label for="nickname">NICKNAME : </label>
        <input id="nickname" type="text" v-model="nickname">
      </div>
      <button v-on:click.prevent="signupUser">sign up</button>
      <button v-on:click.prevent="loginUser">login</button>
    </form>
    
  </div>
</template>

<script>
import axios from 'axios';
export default {
  data() {
      return {
          username: '',
          password: '',
          nickname: '',
      }
  },
  methods:{
      signupUser(){
          const data = {
            username: this.username,
            nickname: this.nickname,
            password: this.password,
          }
          axios.post('http://localhost:3000/signup', data)
          .then((response) =>{
              console.log(response.data);
              this.initForm();
          })
          .catch((error) => {
              console.log(error);
          });
      },
      loginUser(){
           axios.post('http://localhost:3000/login', {
                username: this.username,
                password: this.password,
           })
           .then(response => {
               const token = response.data.token;
               this.$store.commit('setToken', token);
           })
           .catch(error => console.log(error));
      },
      initForm(){
          this.username = '';
          this.password = '';
          this.nickname = '';
      }
  }
}
</script>

<style>
</style>

모던웹(NEMV) 혼자 제작 하기 3기 - 37 몽고 클라우드(ATLAS) 가입 및 테스트