본문 바로가기

책/Vue.js 퀵스타트

vue-router를 이용한 라우팅

vue-router란?

  • SPA(Single Page Application)은 기본적으로 페이지가 하나이기 때문에 URI별로 다른 화면이 나타나도록 구현하기 힘듦
  • 사용자가 요청한 URI 경로에 따라 각각 다른 화면이 랜더링이 필요

제공하는 기능

  • 중첩된 경로, 뷰 매핑 가능
  • 컴포넌트 기반 라우팅 구현
  • Vue.js 의 전환효과(transition) 적용
  • 히스토리모드, 해시모드 사용가능
  • 쿼리스트링, 파라미터, 와일드카드 사용가능

vue-router의 기초

routertest 프로젝트 생성

vue create routertest
cd routertest
yarn add vue-router bootstrap@3.3.x
또는
npm install --save  vue-router bootstrap@3.3.x

src/components/Home.vue ,About.vue, Contacts.vue

<template>
    <div>
      <h1>Abount</h1>
    </div>
</template>
<script>
export default {
    name : 'about'
}
</script>

src/App.vue

<template>
    <div>
      <div class="header">
        <h1 class="headerText">(주)SSG</h1>
      </div>
      <nav>
        <ul>
          <li><router-link to="/home">Home</router-link></li>
          <li><router-link to="/about">About</router-link></li>
          <li><router-link to="/contacts">Contacts</router-link></li>
        </ul>
      </nav>
      <div class="container">
        <router-view></router-view>
      </div>
    </div>
</template>
<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contacts from './components/Contacts.vue'
import VueRouter from 'vue-router';

const router = new VueRouter({
  routes : [
    {path :'/', component : Home},
    {path :'/home', component : Home},
    {path :'/about', component : About},
    {path :'/contacts', component : Contacts}
  ]
});

export default {
    name: 'App',
    router
}
</script>
<style>
.header { background-color: aqua; padding: 10px 0 0 0;}
.headerText { padding: 0 20px;}
ul { 
  list-style-type: none; margin: 0; padding: 0;
  overflow: hidden; background-color: purple;
}
li { float: left;}
li a { 
  display: block; color: yellow; text-align: center;
  padding: 14px 16px; text-decoration: none;
}
li a:hover {background-color: aqua; color: black;}
</style>

src/main.js

import Vue from 'vue'
import App from './App.vue'
import 'bootstrap/dist/css/bootstrap.css';
import VueRouter from 'vue-router';

Vue.config.productionTip = false
Vue.use(VueRouter);

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

동적 라우트

src/ContactList.js

var contactlist = {
    contacts: [
        { no: 1001, name: '김유신', tel: '010-1212-3331', address: '경주' },
        { no: 1002, name: '장보고', tel: '010-1212-3332', address: '청해진' },
        { no: 1003, name: '관창', tel: '010-1212-3333', address: '황산벌' },
        { no: 1004, name: '안중근', tel: '010-1212-3334', address: '해주' },
        { no: 1005, name: '강감찬', tel: '010-1212-3335', address: '귀주' },
        { no: 1006, name: '정몽주', tel: '010-1212-3336', address: '개성' },
        { no: 1007, name: '이순신', tel: '010-1212-3337', address: '통제영' },
        { no: 1008, name: '김시민', tel: '010-1212-3338', address: '진주' },
        { no: 1009, name: '정약용', tel: '010-1212-3339', address: '남양주' }
    ]
};
export default contactlist;

src/components/Contacts.vue

<template>
    <div>
      <h1>연락처</h1>
      <div class="wrapper">
          <div class="box" v-for="c in contacts" :key="c.no">
              <router-link v-bind:to="'/contacts/'+c.no">{{c.name}}</router-link>
          </div>
      </div>
    </div>
</template>
<script>
import contactlist from '../ContactList';
export default {
    name : 'contacts',
    data() {
        return {contacts: contactlist.contacts};
    }
}
</script>
<style>
.wrapper { background-color: #fff; clear:both; display: table;}
.box {
    float: left; background-color: aqua; border-radius: 5px;
    padding: 10px; margin: 3px; text-align: center; font-size: 120%;
    width: 100px; font-weight: bold;
}
a:link, a:visited { 
    text-align: center; text-decoration: none;
    display: inline-block;
}
</style>

src/component/ContactByNo.vue

<template>
    <div>
        <h1>연락처 상세</h1>
        <div>
            <table class="detail table table-borderd">
                <tbody>
                    <tr class="active">
                        <td>일련번호</td>
                        <td>{{contact.no}}</td>
                    </tr>
                    <tr class="active">
                        <td>이름</td>
                        <td>{{contact.name}}</td>
                    </tr>
                    <tr class="active">
                        <td>전화</td>
                        <td>{{contact.tel}}</td>
                    </tr>
                    <tr class="active">
                        <td>주소</td>
                        <td>{{contact.address}}</td>
                    </tr>
                </tbody>
            </table>
        </div>
    </div>
</template>
<script>
import contactlist from '../ContactList';
export default {
    name : 'contactbyno',
    data(){
        return {
            no : 0,
            contacts: contactlist.contacts
        }
    },
    created : function() {
        this.no = this.$route.params.no;
    },
    computed : {
        contact : function(){
            var no = this.no;
            var arr = this.contacts.filter(function(item){
                return item.no == no;
            });
            if(arr.length == 1) return arr[0];
            else return {};
        }
    }
}
</script>
<style>
table.detail { width: 400px;}
</style>

src/App.vue

<script>
import Home from './components/Home.vue'
import About from './components/About.vue'
import Contacts from './components/Contacts.vue'
import ContactByNo from './components/ContactByNo.vue'
import VueRouter from 'vue-router';

const router = new VueRouter({
  routes : [
    {path :'/', component : Home},
    {path :'/home', component : Home},
    {path :'/about', component : About},
    {path :'/contacts', component : Contacts},
    {path :'/contacts/:no', component : ContactByNo}
  ]
});

export default {
    name: 'App',
    router
}
</script>

중첩 라우트

하나의 컴포넌트가 다시 하위 컴포넌트를 포함하는 경우 라우팅 중첩이 가능해야 함

src/App.vue

<script>
....
const router = new VueRouter({
  routes : [
    {path :'/', component : Home},
    {path :'/home', component : Home},
    {path :'/about', component : About},
    {
      path :'/contacts', component : Contacts,
      children : [
        {path :'/contacts/:no', component : ContactByNo}
      ]
    }
  ]
});
....
</script>

src/components/Contacts.vue

<template>
    <div>
      <h1>연락처</h1>
      <div class="wrapper">
....
      </div>
      <router-view></router-view>
    </div>
</template>

src/components/ContactByNo.vue

<template>
    <div>
        <hr class="divider" />
        <h1>연락처 상세</h1>
....
    </div>
</template>
<script>
import contactlist from '../ContactList';
export default {
    name : 'contactbyno',
    data(){
        return {
            no : 0,
            contacts: contactlist.contacts
        }
    },
    created : function() {
        this.no = this.$route.params.no;
    },
    /* 
    이미 ContactByNo Vue 인스턴스가 생성되어 있는 상태이므로 
    더이상 created, mounted 이벤트 훅이 발생하지 않음
    
    따라서 $route속성에 대한 관찰을 수행
     - to : 현재의 라우트 객체
     - from : 이전의 라우트 객체
    */
    watch : {
        '$route' : function(to) {
            this.no = to.params.no;
        }
    },
    computed : {
        contact : function(){
            var no = this.no;
            var arr = this.contacts.filter(function(item){
                return item.no == no;
            });
            if(arr.length == 1) return arr[0];
            else return {};
        }
    }
}
</script>
<style>
table.detail { width: 400px;}
.devider {
    height: 3px; margin-left: auto; margin-right: auto;
    background-color: #ff0066; color: #ff0066; border: 0 none;
}
</style>

명명된 라우트

라우트에 고유한 이름을 부여하는 것

scr/App.vue

<template>
    <div>
...
      <nav>
        <ul>
          <li><router-link :to="{name : 'home'}">Home</router-link></li>
          <li><router-link :to="{name : 'about'}">About</router-link></li>
          <li><router-link :to="{name : 'contacts'}">Contacts</router-link></li>
        </ul>
      </nav>
...
    </div>
</template>
<script>
...

import VueRouter from 'vue-router';
const router = new VueRouter({
  routes : [
    {path :'/', component : Home},
    {path :'/home', name:'home', component : Home},
    {path :'/about', name:'about',component : About},
    {
      path :'/contacts', name:'contacts',component : Contacts,
      children : [
        {path :'/contacts/:no', name:'contactbyno',component : ContactByNo}
      ]
    }
  ]
});
...
</script>

src/components/Contacts.vue

전체 URI 경로 형태 Router-link 예
/contacts/:no <router-link v-bind:to="{name:'contacts, params: {no: 1003}'}">...</router-link>

/contacts/1003
/contacts?pageno=:no <router-link v-bind:to="{name:'contacts, query: {pageno: 2}'}">...</router-link>

/contacts?query=2
<template>
    <div>
      <h1>연락처</h1>
      <div class="wrapper">
          <div class="box" v-for="c in contacts" :key="c.no">
              <router-link v-bind:to="{nane : 'contactbyno',params:{no: c.no}}">{{c.name}}</router-link>
          </div>
      </div>
      <router-view></router-view>
    </div>
</template>

 

프로그래밍 방식의 라우트 제어

라우터 객체의 push 메서드

프로그래밍 방식으로 선언

  • 링크를 클릭하면 바로 이동하는 것이 아니라 사용자의 확인을 받고 이동
  • 이벤트처리르 이용해 이동하기 전에 다른 작업을 함께 수행하는 경우

Vue 인스턴스에서는 this.$router.push() 와 같이 이용

/*
- location: 이동하고자 하는 경로
- completeCallback : 네비게이션이 완료 되었을 때 호출되는 콜백 함수
- abortCallback : 네비게이션을 취소 했을 때 호출되는 콜백 함수
*/
push(location, [, completeCallback] [, abortCallback])


// 문자열 직접 전달
this.$router.push('/home');

// 객체 정보로써 전달
this.$router.push({path : '/about'});

// 명명된 라우트 사용
this.$router.push({name :'contacts', params: {no : 1002}});

// 쿼리 문자열 전달
this.$router.push({path :'/contacts', query: {pageno: 1, pagesize : 5}});

src/components/Contacts.vue

<template>
    <div>
      <h1>연락처</h1>
      <div class="wrapper">
          <div class="box" v-for="c in contacts" :key="c.no">
              <span @click="navigate(c.no)" style="cursor:pointer">[ {{c.name}} ]</span>
          </div>
      </div>
      <router-view></router-view>
    </div>
</template>
<script>
import contactlist from '../ContactList';
export default {
...
    methods: {
        navigate(no) {
            if(confirm("상세정보를 보시겠습니까?")) {
                this.$router.push({name: 'contactbyno', params:{no: no}},function(){
                    console.log("/contacts/"+no + "로 이동완료!");
                });
            }
        }
    }
}
</script>

네비게이션 보호

다른 경로로 리디렉션하거나 네비게이션을 취소하여 애플리케이션의 네비게이션을 보호하는 기능

전역 수준 사용 방법

const router = new VueRouter({...});


/* 라우팅이 일어나기 전에 실행
 - to : 이동하려는 대상 Route 객체
 - from : 이동하기 전의 Route 객체
 - next : 함수형
   - next() : 다음 이벤트 훅으로 이동. 이함수를 호출하지 않으면 다음 이벤트 훅으로 이동하지 않음
   - next(경로) : 지정된 경로로 리디렉션됨. 현재의 네비게이션이 중단되고 새로운 네비게이션이 시작
   - next(error) : next에 전달된 인자가 Error객체라면 네비게이션이 중단되고 router.onError()를
     이용해 등록된 콜백에러가 전달
*/
router.beforeEach((to, from, next) => {

});

// 라우팅이 일어난 후 실행
router.afterEach((to, from) => {

});

라우트 정보 수준 사용 방법

const router = new VueRouter({
    route : [
        {
            path : '',
            compoenents : ,
            // 전역 수준의 beforEach 에서와 동일
            beforeEnter : (to, from, next) => {
            
            }
        }
    ]
});

컴포넌트 수준 사용방법

const Foo = {
    template : `...`,
    
    /*
    랜더링 하는 라우트 이전에 호출되는 훅. 
    호출 시점에는 Vue 인스턴스가 만들어지지 않았기 때문에
    this를 이용할 수 없음에 주의
    */
    beforeRouteEnter(to, from , next) {
    
    },
    
    /*
    현재 경로에서 다른 경로로 빠져나갈 때 호출되는 훅
    */    
    beforeRouteLeave(to, from, next) {
    
    },
    
    /*
    이미 렌더링된 컴포넌트의 경로가 변경될 때 호출되는 훅
    이미 현재 컴포넌트의 Vue 인스턴스가 만들어져 있어서 재사용될 때는 
    beforeRouterEnter 훅은 호출되지 않고 beforeRouterUpdate 훅이 호출됨.
    
    이 훅 대신에 $route 옵션에 관찰속성을 사용할 수 있음
    */    
    beforeRouteUpdate(to, from, next) {
    
    }
}

 

  • beforeRouteEnter에서 Vue 인스턴스를 이용하고 싶다면 콜백 함수를 이용해 비동기 처리 수행
beforeRouterEnter(to, from, next) {
    next((vm) => {
        //vm 이 생성된 vue 인스턴스 참조
    });
}

전체 내비게이션 보호 기능의 실행흐름

  • 내비게이션 시작
  • 비활성화된 컴포넌트가 있다면 보호 기능 호출
  • 전역 수준의 beforeEach 호출
  • 재사용되는 컴포넌트의 beforeRouteUpdate 훅 호출
  • 라우트 정보 수준의 beforeEnter 호출
  • 활성화된 컴포넌트에서 beforeRouteEnter 훅 호출
  • 네비게이션 완료
  • 전역 afterEach 호출
  • DOM 갱신 트리거됨
  • 인스턴스들의 beforeRouterEnter 훅에서 next에 전달된 콜백 호출(콜백 사용 시에만)

src/components/ConstantByNo.vue

<script>
import contactlist from '../ContactList';
export default {
    name : 'contactbyno',
...
    created : function() {
        this.no = this.$route.params.no;
    },
    beforeRouteUpdate(to, from, next){
        console.log("** beforeRouteUpdate");
        this.no = to.params.no;
        next();
    },
    /*
    watch : {
        '$route' : function(to) {
            this.no = to.params.no;
        }
    },
    */
...
}
</script>

src/App.vue

<script>
...
const router = new VueRouter({
  routes : [
...
    {
      path :'/contacts', name:'contacts',component : Contacts,
      children : [
        {
          path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
          beforeEnter : (to, from, next) => {
            console.log("@@ beforeEnter! : " + from.path + "-->" + to.path);
            next();
          }
        }
      ]
    }
  ]
});
router.beforeEach((to, from, next) => {
  console.log("** beforeEach!!");
  next()
});
router.afterEach(() => {
  console.log("** afterEach!!");
});
...
</script>

이동시 /contacts 나 /contacts/:no 형태의 URL만 이동. 라운트 수준의 beforeEnter 구현

  • 조건이 만족하지 않으면 home 으로 이동
  • 직접 url 기입 : /#/about --> /#/contacts/1004  --> /#/home 으로 리디렉션됨
<script>
...
const router = new VueRouter({
  routes : [
...
    {
      path :'/contacts', name:'contacts',component : Contacts,
      children : [
        {
          path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
          beforeEnter : (to, from, next) => {
            console.log("@@ beforeEnter! : " + from.path + "-->" + to.path);
            if(from.path.startWith('/contacts')){
              next();
            } else {
              next('/home');
            }
          }
        }
      ]
    }
  ]
});
...
</script>

라우팅 모드

기본은 해시모드. 해시를 제거하려면 mode 옵션을 history 로 변경

src/App.vue

<script>
...
import NotFound from './components/NotFound';

const router = new VueRouter({
  mode : 'history',
  routes : [
  ...
     ,{path: "*", component: NotFound }
  ]
});
...
</script>

src/components/NotFound.vue

<template>
    <h1>요청하신 경로는 존재하지 않습니다</h1>
</template>

라우트 정보를 속성으로 연결하기

  • 컴포넌트를 라우트 객체에 의존적으로 사용하는 것은 재사용 측면에서 바람직하지 않음
    • ContactByNo.vue 의 this.$route 에 의존적인 부분
  • 라우트 경로에 params 정보를 속성에 연결
    • 속성 정의 : props: true
    • 속성 값 : this.$route.param 정보 할당

src/App.vue

  • props: true 속성 추가
<script>
...
const router = new VueRouter({
  mode : 'history',
  routes : [
...
    {
      path :'/contacts', name:'contacts',component : Contacts,
      children : [
        {
          path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
          props: true
        }
      ]
    }
    ,{path: "*", component: NotFound }
  ]
});
...
</script>

<!--
params 정보가 아닌 query 정보등이 속성에 부여되여야 한다면 함수의 리턴값이 할당되도록 변경
예) /contactbyno?no=1004

<script>
...
const router = new VueRouter({
  mode : 'history',
  routes : [
...
    {
      path :'/contacts', name:'contacts',component : Contacts,
      children : [
        {
          path :'/contacts/:no', name:'contactbyno',component : ContactByNo,
          props: function(route) {
              return { no : route.query.no, path: route.path };
          }
        }
      ]
    }
    ,{path: "*", component: NotFound }
  ]
});
...
</script>
-->

src/components/ContactByNo.vue

  • props : true 인 경우에 route.params 정보에 동일한 속성 할당됨
  • URI /contacts/:no  의 :no 는 props : ['no'] 속서에 전달됨
<script>
import contactlist from '../ContactList';
export default {
    name : 'contactbyno',
    props :['no'],
    data(){
        return {
            // no : 0,
            contacts: contactlist.contacts
        }
    },
    /*
    created : function() {
        this.no = this.$route.params.no;
    },
    beforeRouteUpdate(to, from, next){
        console.log("** beforeRouteUpdate");
        this.no = to.params.no;
        next();
    },
    */
...
}
</script>

<!-- 
props: function(route) { return { no : route.query.no, path: route.path }; } 일경우

다음과 같이 속성이 부여됨
export default {
...
    props :['no','path'],
...
}
-->

연락처 애플리케이션에 라우팅 기능을 제공

contactapp 프로젝트를 변경

 

라우팅 경로 정보

URI 경로 이름 컴포넌트
/   Home.vue 컴포넌트(/home 리디렉션)
/home home Home.vue 컴포넌트
/about about About.vue 컴포넌트
/contacts contacts ContactList.vue 컴포넌트
/contacts/add addcontact ContactForm.vue 컴포넌트(입력)
/contacts/update/:no updatecontact ContactForm.vue 컴포넌트(수정)
/contacts/photo/:no updatephoto UpdatePhoto.vue 컴포넌트(사진변경)

초기 설정 작업

src/components/Home.vue , About.vue

<template>
    <div id="example">
        <div class="panel panel-default">
            <div class="panel-heading">About</div>
            <div class="panel-body">About 화면입니다.</div>
        </div>
    </div>
</template>
<style>
#example {
    margin: 10px auto; max-width: 820px; min-width: 820px;
    padding: 0px; position: relative; font: 13px "verdana";
}
</style>

vue-router 패키지 추가

yarn add vue-router
또는
npm install --save vue-router

vuex 상태 관리 기능 변경

  • vue-router 에서는 currentView, mode 모두 필요하지 않음

src/Constant.js

export default {
    // 변이와 액션 모두 사용
...
    FETCH_CONTACT_ONE: 'fetchContactOne', // 연락처 한건 조회
    INITIALIZE_CONTACT_ONE: 'initializeContactOne', // 연락처 초기화

    // 액션에서만 사용
...
}

src/store/state.js

import CONF from '../Config';

export default {
    // currentView: null,
    // mode: 'add',
    contact: { no: 0, name: '', tel: '', address: '', photo: '' },
    contactlist: { pageno: 1, pagesize: CONF.PAGESIZE, totalcount: 0, contacts: [] }
}

src/store/mutations.js

  • axios를 이용해 외부 API를 요청한 후 수신한 데이터를 FETCH_CONTACTS 변이를 이용해 업데이트 하기 때문에 수장과 삭제에 관한 변이는 등록할 것이 없음
import Constant from '../Constant';

// 상태를 변경하는 기능만 구현
export default {
    [Constant.FETCH_CONTACTS]: (state, payload) => {
        state.contactlist = payload.contactlist;
    },
    [Constant.FETCH_CONTACT_ONE]: (state, payload) => {
        state.contact = payload.contact;
    },
    [Constant.INITIALIZE_CONTACT_ONE]: (state) => {
        state.contact = { no: '', name: '', tel: '', address: '', photo: '' };
    }
}

src/store/actions.js

import Constant from '../Constant';
import axios from 'axios';
import CONF from '../Config';
export default {
    [Constant.FETCH_CONTACTS]: (store, payload) => {
        var pageno;
        if (typeof payload === "undefined" || typeof payload.pageno === "undefined") {
            pageno = 1;
        } else {
            pageno = payload.pageno;
        }
        var pagesize = store.state.contactlist.pagesize;

        axios.get(CONF.FETCH, {
            params: { pageno: pageno, pagesize: pagesize }
        }).then((response) => {
            store.commit(Constant.FETCH_CONTACTS, { contactlist: response.data });
        });
    },
    [Constant.ADD_CONTACT]: (store) => {
        axios.post(CONF.ADD, store.state.contact).then((response) => {
            if (response.data.status === 'success') {
                //store.dispatch(Constant.CANCEL_FORM);
                store.dispatch(Constant.FETCH_CONTACTS, { pageno: 1 });
            } else {
                console.log("연락처 추가 실패!! : " + response.data);
            }
        });
    },
    [Constant.UPDATE_CONTACT]: (store) => {
        var currentPageNo = store.state.contactlist.pageno;
        var contact = store.state.contact;
        axios.put(CONF.UPDATE.replace('${no}', contact.no), contact).then((response) => {
            if (response.data.status == 'success') {
                //store.dispatch(Constant.CANCEL_FORM);
                store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
            } else {
                console.log("연락처 변경 실패 : " + response.data);
            }
        });
    },
    [Constant.UPDATE_PHOTO]: (store, payload) => {
        var currentPageNo = store.state.contactlist.pageno;
        var data = new FormData();
        data.append('photo', payload.file);
        axios.post(CONF.UPDATE_PHOTO.replace("${no}", payload.no), data).then(() => {
            //store.dispatch(Constant.CANCEL_FORM);
            store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
        });
    },
    [Constant.DELETE_CONTACT]: (store, payload) => {
        var currentPageNo = store.state.contactlist.pageno;
        axios.delete(CONF.DELETE.replace("${no}", payload.no)).then(() => {
            store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
        });
    },
    // 새로 추가된 부분
    [Constant.FETCH_CONTACT_ONE]: (store, payload) => {
        axios.get(CONF.FETCH_ONE.replace("${no}", payload.no)).then((response) => {
            store.commit(Constant.FETCH_CONTACT_ONE, { contact: response.data });
        });
    },
    [Constant.INITIALIZE_CONTACT_ONE]: (store) => {
        store.commit(Constant.INITIALIZE_CONTACT_ONE);
    }
}

main.js 에 라우팅 기능 추가

import Vue from 'vue'
import App from './App.vue'
import store from './store';
import VueRouter from 'vue-router';

import Home from './components/Home';
import About from './components/About';
import ContactList from './components/ContactList';
import ContactForm from './components/ContactForm';
import UpdatePhoto from './components/UpdatePhoto';

import 'bootstrap/dist/css/bootstrap.css';
import ES6Promise from 'es6-promise';

ES6Promise.polyfill();
Vue.use(VueRouter);
Vue.config.productionTip = false;

var router = new VueRouter({
    routes: [
        { path: '', redirect: '/home' },
        { path: '/home', name: 'home', component: Home },
        { path: '/about', name: 'about', component: About },
        { path: '/contacts',name: 'contacts',component: ContactList, children: [
            { path: '/add', name: 'addcontact', component: ContactForm },
            { path: '/update/:no', name: 'updatecontact', component: ContactForm, props: true },
            { path: '/photo/:no', name: 'updatephoto', component: UpdatePhoto, props: true }
        ]}
    ]
});


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

App.vue 컴포넌트 변경

<template>
  <div id="container">
        <div class="page-header">
          <h1 class="text-center">연락처 관리 애플리케이션</h1>
          <p>(Dynamic Component + EventBus + Axios)</p>
        </div>
        <!--
        <component :is="currentView"></component>
        -->
        <div class="btn-group">
          <router-link to="/home" class="btn btn-info menu">Home</router-link>
          <router-link to="/about" class="btn btn-info menu">About</router-link>
          <router-link to="/contacts" class="btn btn-info menu">Contacts</router-link>
        </div>
        <router-view></router-view>
        <!--
        <contact-list></contact-list>
        -->
  </div>
</template>

<script>
export default {
  name: 'App'
}
</script>

ContactList.vue 컴포넌트 변경

<template>
    <div>
        <p class="addnew">
            <router-link class="btn btn-primary" v-bind:to="{ name : 'addcontact'}" >
            새로운 연락처 추가하기
            </router-link>
        </p>
        <div id="example">
            <table id="list" class="table table-striped table-bordered table-hover">
...
            </table>
        </div>
        <paginate ref="pagebuttons"
            :page-count="totalpage"
            :page-range="7"
            :margin-pages="3"
            :click-handler="pageChanged"
            :prev-text="'이전'"
            :next-text="'다음'"
            :container-class="'pagination'"
            :page-class="'page-item'">
        </paginate>
        <router-view></router-view>
    </div>
</template>
<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
import Paginate from 'vuejs-paginate';

export default {
    name : 'contactList',
    components : { Paginate },
    computed : {
        totalpage : function() {
            return Math.floor((this.contactlist.totalcount-1) / this.contactlist.pagesize) + 1;
        },
        ...mapState(['contactlist'])
    },
    mounted : function(){
        var page = 1;
        if(this.$route.query && this.$route.query.page){
            page = parseInt(this.$route.query.page);
        }

        this.$store.dispatch(Constant.FETCH_CONTACTS,{pageno:page});
        this.$refs.pagebuttons.selected = page-1;
    },
    watch : {
        '$route' : function(to, from) {
            if(to.query.page && to.query.page != this.contactlist.pageno){ 
                var page = to.query.page;
                this.$store.dispatch(Constant.FETCH_CONTACTS, {pageno, page});
                this.$refs.pagebuttons.selected = page-1;
            }
        }
    },
    methods : {
        pageChanged : function(page) {
            this.$router.push({name : 'contacts', query : {page: page}});
        },
        editContact : function(no) {
            this.$router.push({name : 'updatecontact', params : {no : no}});
        },
        deleteContact : function(no) {
            if(confirm("정말로 삭제하시겠습니까?")){
                this.$store.dispatch(Constant.DELETE_CONTACT,{no:no});
                //this.$router.push({name : 'contacts'});
                this.$router.go(this.$router.currentRouter);
            }
        },
        editPhoto : function(no) {
            this.$router.push({name : 'updatephoto', params: {no:no }});
        }
    }
}
</script>

ContactForm.vue, UpdatePhoto.vue 컴포넌트 수정

ContactForm.vue

<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';
export default {
    name : 'contactForm',
    data() {
        return {mode : 'add'};
    },
    props: ['no'],
    computed : {
        btnText : function() { 
            if(this.mode != 'update') return '추 가';
            else return '수 정';
        },
        headingText : function() {
            if(this.mode != 'update') return '새로운 연락처 추가';
            else return '연락처 변경';
        },
        ...mapState(['contact','contactlist'])
    },
    mounted : function() {
        this.$refs.name.focus();

        var cr = this.$router.currentRoute;
        if(cr.fullPath.indexOf('/add') > -1){
            this.mode ='add';
            this.$store.dispatch(Constant.INITIALIZE_CONTACT_ONE);
        } else if(cr.fullPath.indexOf('/update') > -1) {
            this.mode ='update';
            this.$store.dispatch(Constant.FETCH_CONTACT_ONE,{no : this.no});
        }
    },
    methods : {
        submitEvent : function() {
            if(this.mode == 'update'){
                this.$store.dispatch(Constant.UPDATE_CONTACT);
                this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
            }else {
                this.$store.dispatch(Constant.ADD_CONTACT);
                this.$router.push({name: 'contacts', query: {page: 1}});
            }
        },
        cancelEvent : function() {
            //this.$store.dispatch(Constant.CANCEL_FORM);
            this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
        }
    }
}
</script>

UpdatePhoto.vue

<script>
//import eventBus from '../EventBus.js';
import Constant from '../Constant';
import {mapState} from 'vuex';

export default {
    name : 'updatePhoto',
    props: ['no'],
    computed : mapState(['contact', 'contactlist']),
    mounted : function() {
        this.$store.dispatch(Constant.FETCH_CONTACT_ONE,{no : this.no});
    },
    methods: {
        cancelEvent : function() {
            //this.$store.dispatch(Constant.CANCEL_FORM);
            this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});
        },
        photoSubmit : function() {
            var file = this.$refs.photofile.files[0];
            this.$store.dispatch(Constant.UPDATE_PHOTO,{no:this.contact.no, file:file});
            this.$router.push({name: 'contacts', query: {page: this.contactlist.pageno}});

        }
    }
}
</script>

 

지연 시간에 대한 처리

API 호출 지연 시간 발생

src/Config.js 오래 시간이 걸리는 api로 변경

let BASE_URL = "/api";

export default {
    // 한 페이지에 보여줄 페이지 사이즈
...
    // 전체 연락처 데이터 요청
    FETCH: BASE_URL + "/contacts_long",
   ...
}

스피너 컴포넌트 작성

yarn add vue-simple-spinner
또는 
npm install --save vue-simple-spinner

Loading.vue

<template>
     <div class="modal">
         <spinner class="spinner" :line-size="10" :size="100">
         </spinner>
     </div>    
</template>
<script>
import Spinner from 'vue-simple-spinner';
export default {
    components : {
        Spinner
    }
}
</script>
<style scoped>
.modal {
    display: block; position: fixed; z-index: 1;
    left: 0; top: 0; width: 100%; height: 100;
    overflow: auto; background-color: rgb(0,0,0); 
    background-color: rgba(0,0,0,0.4); 
}
.spinner {
    position: absolute; left: 50%; top: 50%;
    margin-top: -50px; margin-left: 50px;
}
</style>

Constant.js

export default {
    // 변이와 액션 모두 사용
...
    INITIALIZE_CONTACT_ONE: 'initializeContactOne', // 연락처 초기화
    CHANGE_ISLOADING: 'changeIsLoading', // 스피너UI를 보여줄지 여부 결정

    // 액션에서만 사용
...
}

src/store/state.js

import CONF from '../Config';

export default {
    // currentView: null,
    // mode: 'add',
    isloading: false,
    contact: { no: 0, name: '', tel: '', address: '', photo: '' },
    contactlist: { pageno: 1, pagesize: CONF.PAGESIZE, totalcount: 0, contacts: [] }
}

src/store/mutations.js

import Constant from '../Constant';

// 상태를 변경하는 기능만 구현
export default {
    [Constant.FETCH_CONTACTS]: (state, payload) => {
        state.contactlist = payload.contactlist;
    },
    [Constant.FETCH_CONTACT_ONE]: (state, payload) => {
        state.contact = payload.contact;
    },
    [Constant.INITIALIZE_CONTACT_ONE]: (state) => {
        state.contact = { no: '', name: '', tel: '', address: '', photo: '' };
    },
    [Contant.CHANGE_ISLOADING]: (state, payload) => {
        state.isloading = payload.isloading;
    }
}

src/store/actions.js

import Constant from '../Constant';
import axios from 'axios';
import CONF from '../Config';
export default {
    [Constant.CHANGE_ISLOADING]: (store, payload) => {
        store.commit(Constant.CHANGE_ISLOADING, payload);
    },
    [Constant.FETCH_CONTACTS]: (store, payload) => {
        var pageno;
        if (typeof payload === "undefined" || typeof payload.pageno === "undefined") {
            pageno = 1;
        } else {
            pageno = payload.pageno;
        }
        var pagesize = store.state.contactlist.pagesize;
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.get(CONF.FETCH, {
            params: { pageno: pageno, pagesize: pagesize }
        }).then((response) => {
            store.commit(Constant.FETCH_CONTACTS, { contactlist: response.data });
            store.dispatch(Constant.CHANGE_ISLOADING, { isloading: false });
        });
    },
    [Constant.ADD_CONTACT]: (store) => {
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.post(CONF.ADD, store.state.contact).then((response) => {
            if (response.data.status === 'success') {
                //store.dispatch(Constant.CANCEL_FORM);
                store.dispatch(Constant.FETCH_CONTACTS, { pageno: 1 });
            } else {
                console.log("연락처 추가 실패!! : " + response.data);
            }
        });
    },
    [Constant.UPDATE_CONTACT]: (store) => {
        var currentPageNo = store.state.contactlist.pageno;
        var contact = store.state.contact;
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.put(CONF.UPDATE.replace('${no}', contact.no), contact).then((response) => {
            if (response.data.status == 'success') {
                //store.dispatch(Constant.CANCEL_FORM);
                store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
            } else {
                console.log("연락처 변경 실패 : " + response.data);
            }
        });
    },
    [Constant.UPDATE_PHOTO]: (store, payload) => {
        var currentPageNo = store.state.contactlist.pageno;
        var data = new FormData();
        data.append('photo', payload.file);
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.post(CONF.UPDATE_PHOTO.replace("${no}", payload.no), data).then(() => {
            //store.dispatch(Constant.CANCEL_FORM);
            store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
        });
    },
    [Constant.DELETE_CONTACT]: (store, payload) => {
        var currentPageNo = store.state.contactlist.pageno;
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.delete(CONF.DELETE.replace("${no}", payload.no)).then(() => {
            store.dispatch(Constant.FETCH_CONTACTS, { pageno: currentPageNo });
        });
    },
    // 새로 추가된 부분
    [Constant.FETCH_CONTACT_ONE]: (store, payload) => {
        store.dispatch(Constant.CHANGE_ISLOADING, { isloading: true });
        axios.get(CONF.FETCH_ONE.replace("${no}", payload.no)).then((response) => {
            store.commit(Constant.FETCH_CONTACT_ONE, { contact: response.data });
            store.dispatch(Constant.CHANGE_ISLOADING, { isloading: false });
        });
    },
    [Constant.INITIALIZE_CONTACT_ONE]: (store) => {
        store.commit(Constant.INITIALIZE_CONTACT_ONE);
    }
}

src/App.vue

<template>
  <div id="container">
....
        <router-view></router-view>
        <loading v-show="isloading"></loading>
  </div>
</template>

<script>
import Loading from './components/Loading';
import {mapState} from 'vuex';
export default {
  name: 'App',
  components: {Loading},
  computed : mapState(['isloading'])
}
</script>

' > Vue.js 퀵스타트' 카테고리의 다른 글

단위 테스트  (0) 2020.11.28
트랜지션 효과  (0) 2020.11.28
Vuex를 이용한 상태관리  (0) 2020.11.27
axios를 이용한 서버통신  (0) 2020.11.25
컴포넌트 심화  (0) 2020.11.23