웹 페이지 렌더링에 드는 비용은 크게 두 가지로 나눌 수 있다.
첫 번째로 DOM객체 조작 비용이 있고, 두 번째로 DOM조작 시 페이지를 다시 렌더링 하는 비용이 있다. 가상 DOM을 사용하면 두 가지 비용을 모두 줄일 수 있다.
직접 다운받아 <script>에 추가하거나 CDN 사용
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>
NPM (Webpack 또는 Browserify와 같은 모듈 번들러와 사용)
$ npm install vue
*참고 : Webpack이란? (https://nesoy.github.io/articles/2019-02/Webpack) \
CLI (Vue를 위한 스캐폴딩 도구)
npm install -g yarn @vue/cli
(Webpack 사용하지 않는 경우)
<script type="text/javascript">
var viewModel = new Vue({
el : '#containerElementId'
data : {
name : 'song'
age : 27
}
})
</script>
속성(옵션, 옵션객체) | 설명 |
el | Vue인스턴스에 연결할 HTML DOM요소 지정 (한 개만 가능) |
data | model 데이터를 의미, Vue 인스턴스 내부에서 직접 이용되지 않고 Vue 인스턴스와 data 옵션에 주어진 객체 사이에 프록시를 두어 처리하므로 동기화가 가능 |
computed (계산형 속성) | Vue객체(인스턴스)의 속성 중 computed를 사용하여 함수를 등록하면 객체 속성처럼 이용할 수 있다.
ex) computed오브젝트에 등록된 filtered함수가 특정 배열에서 필터링 된 값들을 반환하는 경우 : <tr v-for=”c in filtered”></tr> 계산형 속성을 객체 형태로 만들어 내부에 get과 set 함수를 만들어 분리해 사용할 수 있다. 자바의 getter와 setter같은 역할이며 꼭 return해주지 않아도 된다 (추측하기로는 정의된 이름으로 사용하기 때문에 꼭 반환하지 않아도 되는듯?) 종속된 값에 의해 결과값이 캐싱된다는 점이 methods와 가장 큰 차이점이다. 변화가 없다면 캐싱된 결과값을 바로 리턴하는 장점이 있다. |
methods | Vue객체(인스턴스)에서 사용할 메서드를 등록하는 옵션
등록된 메서드는 Vue인스턴스를 통해 직접 호출할 수도 있고, 템플릿 표현식으로도 사용할 수 있다. 계산형 속성과 다르게 메서드기 때문에 호출 구문 형식을 사용해야함 결과값을 캐싱하지 않는다는 점이 computed(계산형 속성)과의 가장 큰 차이점이다. 호출할 때마다 매번 메서드를 실행하여 결과값을 리턴한다. 이벤트 매핑 시 (v-on 혹은 @) Vue.js의 이벤트 객체는 W3C 표준 HTML DOM Event모델을 그대로 따르면서 추가적인 속성을 제공, 바닐라 자바스크립트에서 사용하던 이벤트 정보를 대부분 그대로 사용 가능(ex2) ex1) methods에 total함수가 정의된 경우 : <span>{{total()}}</span> ex2) methods : { conlog : function (e) { console.log(e.target); console.log(e.currentTarget); } } |
watch (관찰 속성) | 한 data의 상태 변화를 감지하여 호출될 함수 지정
긴 시간이 필요한 비동기 처리 시 유용하다 (콜백 느낌으로다가..) computed는 동기 처리에만 사용이 가능하다고 함 |
Vue인스턴스로 선언된 변수명에 .$식별자를 통해 직접 접근할 수 있다.
$options 속성을 이용하면 인스턴스의 모든 옵션 정보를 접근할 수도 있다.
그러나 프록시 처리가 된(안정적인) 접근을 위해서 직접 접근을 지양하는 것이 좋다.
ex) data옵션에 접근 : ewModel.$data.name
콧수염 표현식(Mustache Expression)으로, {{message}}와 같이 템플릿 구문을 사용하여 HTML DOM에 데이터를 렌더링한다. ViewModel 객체의 data 속성에서 해당 값을 가져온다. 이를 선언적 렌더링이라고 한다.
v-접두어로 시작하며 HTML element에 속성으로 줄 수 있다. Vue에서 제공하는 특수 속성이다. 표로 정리하면 다음과 같다.
디렉티브 | 설명 | 비고 |
v-text | innerText 속성에 연결 | 단방향 바인딩 |
v-html | innerHtml 속성에 연결 | 단방향 바인딩 |
v-bind | 요소의 콘텐츠 영역이 아닌 요소 객체의 속성을 바인딩.
생략하여 작성하는 것이 가능하다 (ex3) data옵션에 스타일 객체를 만들어 바인딩 할 수도 있고(ex4), 스타일 객체 여러 개를 배열로 바인딩 할 수도 있다(ex5). class를 바인딩 할 수 있다. 개별적으로 하거나(ex6), 객체를 바인딩하여 사용할 수 있다(ex7). boolean값으로 true인 경우 해당 클래스를 추가하고, 아닌 경우 제외시킨다. 컴포넌트에 전달할 임의의 값을 바인딩할 수도 있다(ex8). ex1) v-bind:value=”input 문자열” ex2) v-bind:src=”http://abc.def/imageSample.jpg” ex3) :value=”input 문자열” ex4) :style=”style1” ex5) :style=”[style1, style2]” ex6) :class=”{class1 : c1, class2 : c2, class3 : c3}” ex7) :class=”classObj” ex8) :headerText=”바인딩할내용” |
단방향 바인딩 |
v-model | 양방향 데이터 바인딩 할 때 사용
다중 선택 가능한 checkbox등은 모델 객체의 배열과 연결 lazy, number, trim 등의 수식어를 지원 ex1) v-model.lazy=”name” ex2) v-model.number=”age” ex3) v-model.trim=”message” |
양방향 바인딩
한글 처리시 v-on 디렉티브를 이용 권장 |
v-show | 조건에 따라 display속성을 변경 | 렌더링 O |
v-if, v-else,
v-else-if |
조건에 따라 렌더링 여부를 결정 | 렌더링 X |
v-for | 반복 렌더링 시 사용
원본 데이터가 배열 또는 유사배열인 경우 in 구문으로 사용하고(ex1), 객체인 경우 Key, Value값을 얻어낼 수 있는 구조를 사용(ex2) v-bind를 이용해 key속성값을 주면 해당 DOM요소를 추적하여 관리할 수 있다(ex5) ex1) 인덱스 없이 표현할 객체의 배열 <tr v-for=”content in contents”> <td>{{content.name}}</td> </tr> ex2) 인덱스 없이 표현할 객체 <option v-for=”(val, key) in regions” v-bind=”key”>{{val}}</option> ex3) 인덱스를 사용하는 객체의 배열 <tr v-for=”(content, index) in contents”></tr> ex4) 인덱스를 사용하는 객체 <option v-for=”(val, key, index) in regions” v-bind=”key”>{{val}}</option> ex5) v-bind로 key값 부여 <tr :key=”contact.primaryKeyNo”></tr> |
|
v-pre | HTML요소에 대한 컴파일을 수행하지 않음
문자열을 그대로 출력하기 위해 사용 |
|
v-once | HTML요소를 단 한 번만 렌더링하도록 설정 | |
v-cloak | 컴파일이 완료되지 않은 채로 템플릿 문자열이 화면에 그대로 나타나는 것을 방지하기 위해 사용
컴파일되지 않은 템플릿을 나타내지 않음 |
|
v-on | 이벤트 처리시 사용, v-on:click과 같이 사용
vue인스턴스의 data 변수명을 직접 사용할 수 있음 : 인라인 이벤트 처리(ex1) v-bind처럼 생략하여 작성하는 것이 가능(ex2) 뷰 인스턴스의 methods에 선언된 이벤트 처리 함수와 연결 가능하며, 호출 형식으로 사용하지 않아도 된다. 아마도 내부적으로 변수가 함수인지 여부를 체크하는 듯. 이 사용법을 권장(ex3) ex1) <button v-on:click=”balance += parseInt(amount)”> ex2) <button @click=”balance += parseInt(amount)”> ex3) <button @click=”doCalculate”> |
렌더링 내용에는 포함되지 않으나 요소들을 그룹으로 묶기 위해 사용하는 태그
조건이나 반복등을 지정하여 응용할 수 있다.
<template v-for=”(contact, index) in contacts”></template> 과 같이 사용
루트 요소를 반드시 하나만 가지고 있어야 한다.
Vue 컴포넌트 생성 관리시 중요
또한 아래 사이클에 Vue 인스턴스에서 훅을 걸어 처리할 수도 있다(속성으로 줄 수 있다).
사이클 (2개씩 묶음) | 설명 |
beforeCreate → created | 인스턴스 생성과 관찰/계산형 속성/메서드/감시자 설정 관련 |
beforeMount → mounted | el에 인스턴스 데이터 마운트 관련 |
beforeUpdate → updated | 가상DOM 렌더링 및 DOM업데이트(패치) 관련 |
beforeDestroy → destroyed | 인스턴스 제거 관련 |
참고 : vue 라이프사이클 이미지 주소 https://kr.vuejs.org/images/lifecycle.png
이벤트 수식어는 인라인으로 v-on:eventname.수식어 와 같이 사용한다.
ex) @click.stop=”do” , v-on:click.prevent=”do”
수식어 | 설명 |
.prevent | preventDefault() 역할
ex) <div v-on:contextmenu.prevent=”dosomething”></div> |
.stop | stopPropagation() 역할
ex) <div @click.stop=”dosomething”></div> |
.capture | CAPTURING_PHASE 단계에서만 이벤트 발생 |
.self | RAISING_PHASE 단계에서만 이벤트 발생 |
.once | 이벤트를 단 한 번만 수행할 때 사용 |
키코드 수식어 | 키보드 이벤트와 관련된 수식어, 입력된 키코드에 매핑
@keyup.13=”” 혹은 @keyup.ctrl.67=”” 과 같이 사용 여러 개를 결합하여 사용이 가능함 수로써 키코드를 매핑할 수도 있지만, 뷰에서는 다음 별칭도 제공한다. .enter / .tab / .delete / .esc / .space / .up / .down / .left / .right / .ctrl / .alt / .shift / .meta |
마우스 수식어 | 기본적인 설명은 키코드 수식어와 같음
.left / .right / .middle |
Vue.js는 컴포넌트들을 조합해 전체 애플리케이션을 작성한다. 컴포넌트는 부모-자식 관계로 트리 구조를 형성한다.
부모(상위)컴포넌트는 자식 컴포넌트로 정보(props)를 전달하고,
자식(하위)컴포넌트는 부모 컴포넌트로 이벤트를 전달한다.
.sync 수식어를 통해 양방향 정보 전달이 가능하지만 권장하지 않는 방법이므로 위와 같이 알아두는 것이 좋다. (버전에 따라 상이하고, 일관적이고 명백하지 않기 때문)
컴포넌트 기반 개발 시 data 옵션은 각 컴포넌트의 로컬 상태를 관리하기 위한 용도로만 사용한다. 또한 하나의 컴포넌트를 애플리케이션에서 여러 번 사용할 경우에 모두 다른 상태 정보를 가져야 한다. 단순 객체 값으로 작성하는 경우 객체가 참조형이므로 모두 동일한 참조값을 가져 다른 상태정보를 가질 수 없다. 따라서 컴포넌트에서 data 옵션은 반드시 객체를 리턴하는 함수로 작성해야 한다.
Vue.component(tagname, options)
Vue.component('hello-component', {
template: '<div>컴포넌트 테스트 안녕하세요 ㅎㅎ</div>'
})
Vue.component('hello-component', {
template: '#templateId'
})
<body>
<template id="templateId">
<div>
안녕하세요 컴포넌트 테스트 템플릿아이디로 대신
</div>
</template>
</body>
<script type="text/x-template" id="templateId2">
<div>스크립트로 생성</div>
</script>
속성을 통해서 정보를 전달하기 위해서는 특성(Attribute)처럼 전달한다
대소문자를 구분하지 않기에 반드시 케밥 표기법으로 사용해야 한다
<template id="listTemplate">
<li>{{message}}</li>
</template>
<script type="text/javascript">
Vue.component('list-component', {
template : '#listTemplate',
props : ['message']
})
</script>
<list-component message="헬로"></list-component>
<list-component message="hello"></list-component>
또한 props는 객체 형태로 사용할 수도 있다.
객체 형식으로 지정하는 경우 type과 필수여부, 기본값 등을 지정할 수 있다.
<script type="text/javascript">
Vue.component('list-component', {
template : '#listTemplate',
props : {
message : { type : String, default : '안녕하세요' },
count : { type : Number, required : true }
}
})
</script>
전달할 값이 배열이나 객체인 경우에 default값을 부여하려면 반드시 함수를 이용해야 한다.
<script type="text/javascript">
Vue.component('list-component', {
template : '#listTemplate',
props : {
countries : {
type: Array,
default : function() {
return ['대한민국'];
}
}
}
})
</script>
<list-component v-bind:countries="['미국', '영국', '호주']"></list-component>
자식 컴포넌트에서 이벤트를 발신(emit) 하면 부모 컴포넌트는 v-on디렉티브로 수신
아래 예제에서 timeclick 이란 이벤트는 임의로 정한 이름인 것 같다.
자식 컴포넌트에서 v-on:click으로 캐치해서 $emit할 때 해당 이름으로 던져주는 것
[자식 컴포넌트]
<template id="childTemplate">
<div>
<button class="buttonstyle" v-on:click="clickEvent"
v-bind:data-lang="buttonInfo.value">
{{buttonInfo.text }}
</button>
</div>
</template>
<script type="text/javascript">
Vue.component('child-component', {
template : '#childTemplate',
props : [ 'buttonInfo' ],
methods : {
clickEvent : function(e) {
this.$emit('timeclick',
e.target.innerText,
e.target.dataset.lang);
}
}
})
</script>
[부모 컴포넌트]
<template id="parent-template">
<div>
<child-component v-for="s in buttons" v-bind:button-info="s"
v-on:timeclick="timeclickEvent">
</child-component>
<hr />
<div>{{ msg }}</div>
</div>
</template>
<script type="text/javascript">
Vue.component('parent-component', {
template : '#parent-template',
props : [ 'buttons' ],
data : function() {
return { msg:"" };
},
methods : {
timeclickEvent : function(k, v) {
this.msg = k + ", " +v;
}
}
})
</script>
UI를 만들기 위한 템플릿 문자열이나 데이터 옵션을 포함하지 않는 Vue 인스턴스를 이용하여 부모-자식 관계가 아닌 컴포넌트들 사이에도 정보를 전달할 수 있도록 하며, 이를 이벤트 버스 객체라고 한다.(1)
부모-자식 컴포넌트 관계와 마찬가지로 먼저 이벤트를 캐치할 컴포넌트에서 $on으로 바인딩한다. 컴포넌트 생명주기 created훅에서 해당 작업을 수행한다.(2)
이벤트를 발생시킬 컴포넌트에서는 $emit을 통해 이벤트를 전달한다.(3)
var eventBus = new Vue();
created : function() {
eventBus.$on('click1', this.child1Click);
},
methods : {
child1Click : function(time) {
this.timelist.push(time);
}
}
eventBus.$emit('click1', t);
*Vue CLI와 관련있으니 함께 읽는 편이 좋다.
단일 파일 컴포넌트(Single File Component)는 전역 수준 컴포넌트의 몇 가지 문제점을 해결한다. 전역 수준 컴포넌트의 문제점은 다음과 같다.
정리하자면, 단일 파일 컴포넌트로 빌드하는 경우 여러 브라우저에 호환이 가능하도록 만들기 때문에 최신 자바스크립트 문법이 사용 가능하고, CSS스타일을 빌드하고 모듈화 해서 관리할 수 있으며, 식별이 쉽고 식별자의 충돌을 쉽게 피할 수 있다는 것이다.
Vue CLI를 이용해 생성한 프로젝트 코드는 vue-loader라는 npm 패키지를 이용해 단일 파일 컴포넌트를 지원한다.
확장자가 .vue인 파일에 <template>, <script>, <style>을 작성하면 vue-loader는 이 파일을 파싱하고 다른 로더(loader)들을 활용해 하나의 모듈로 조립한다.
css-loader를 이용해 CSS스타일을 전처리할 수 있으며, 스타일 정보를 모듈화 할 수 있다.
단일 파일 컴포넌트는 전역 파일 컴포넌트와 비교했을 때 다음과 같은 차이점을 가지고 사용한다.
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
<script>
import HelloWorld from './components/HelloWorld.vue'
export default {
name: 'App',
components: {
HelloWorld
}
}
</script>
App.vue의 구조다. App.vue는 가장 최상위 컴포넌트다.
<template>에 아이디를 특별히 지정하지 않는 것을 확인할 수 있고, Vue.component()로 이름과 template 속성도 지정하지 않았다.
import Vue from 'vue'
import TodoList from './components/TodoList.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(TodoList),
}).$mount('#app')
main.js의 구조다. main.js는 가장 먼저 실행되는 javascript 파일로, Vue 인스턴스를 생성하는 역할을 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
public/index.html의 구조다.
[이벤트 버스 사용법]
import Vue from 'vue';
var eventBus = new Vue();
export default eventBus;
EventBus.js의 구조다. 앞서 배운것과 마찬가지로 빈 껍데기 Vue 인스턴스를 생성하여 $on과 $emit을 이용해 사용한다.
<style>
* { box-sizing: border-box; }
.header {
background-color: purple; padding: 30px 30px;
color: yellow; text-align: center;
}
.header:after {
content: ""; display: table; clear: both;
}
</style>
<template>
<div id="todolistapp">
<div id="header" class="header">
<h2>Todo List App</h2>
<input-todo />
</div>
<list></list>
</div>
</template>
<script type="text/javascript">
import InputTodo from './InputTodo';
import List from './List';
export default {
name : 'todo-list',
components : { InputTodo, List }
}
</script>
src/components/TodoList.vue의 구조다. 앞서 배운 대로 <style>을 따로 가지고 있는 것을 볼 수 있다. 또한 따로 만들어 둔 컴포넌트를 import 해서 사용하는 것을 볼 수 있다. export 할 객체의 옵션인 components에 등록해야 함을 볼 수 있다.
<input-todo/>는 InputTodo.vue 파일에서 가져온 것이며, export default 객체에서 name옵션을 ‘input-todo’로 등록했다. 그렇기 때문에 import하는 쪽인 TodoList.vue 파일에서 다음과 같은 태그명으로 사용할 수 있는 것이다.
컴포넌트에서 <style>로 적용한 CSS는 전역 범위로 설정된다. 동일한 이름으로 설정된 스타일이 있을 경우 마지막에 등록된 스타일만이 적용된다.
이러한 스타일 중복을 피하기 위해 범위를 지정하여 해당 컴포넌트에만 스타일을 주고 싶다면 scoped를 사용하면 된다.
<style scoped>
원리는 빌드 시 내부적으로 컴포넌트 안에 속한 요소(element)들에 data-v-xxxxx 형태의 특성(Attribute)을 부여한 뒤에 스타일 먹일 때 식별자로써 사용하는 것이다.
그러므로 css선택자는 특성 선택자를 사용하기 때문에 스타일 적용하는 속도가 현저히 느리다. id나 class 등으로 직접 지정하는 편이 속도에 이점이 있다.
또한 이런 범위 CSS는 하위 컴포넌트에도 반영이 된다(같은 data-v-xxxxx를 갖는다)는 점을 유의하고 사용해야 한다.
CSS 모듈은 CSS 스타일을 객체처럼 다룰 수 있게 한다.
<style module></style>
과 같이 사용할 수 있다.
<template>
<div>
<button v-bind:class="$style.hand"> CSS Module을 적용한 버튼 </button>
<div :class="[$style.box, $style.border]">Hello World</div>
</div>
</template>
<script>
export default {
created() {
console.log(this.$style);
}
}
</script>
<style module>
.hand { cursor:pointer; background-color:purple; color:yellow; }
.box { width:100px; height:100px; background-color:aqua; }
.border { border:solid 1px orange; }
</style>
스타일을 모듈화한 예시다. 모듈화하게 되면 Vue 인스턴스 내에서 $style이라는 계산형 속성을 통해 이용할 수 있다. 여러 개라면 배열 형태로 사용할 수 있다.
자식 컴포넌트와 정보 교환 방법으로 props를 사용하는 경우 HTML 태그가 포함된 문자열을 전달하기 쉽지 않다. 자식 컴포넌트에서 마크업으로 렌더링 작업을 거쳐야 하기 때문이다.
슬롯(slot)은 이러한 불편함을 해결하는 방법이다. 슬롯을 이용해 부모 컴포넌트에서 자식 컴포넌트로 HTML 마크업을 전달할 수 있다.
자식 컴포넌트에서는 <slot></slot> 태그를 작성하고,
부모 컴포넌트에서는 콘텐츠 영역에 자식 컴포넌트의 <slot></slot>영역에 나타낼 HTML 마크업을 작성하면 된다.
<template>
<div class="container">
<div class="header">{{headerText}}</div>
<div class="content">
<slot></slot>
</div>
<div class="footer">{{footerText}}</div>
</div>
</template>
<script>
export default {
props : [ 'headerText', 'footerText' ]
}
</script>
src/components/SpeechBox.vue의 내용이다. 각각 부모로부터 전달받을 text들을 정의해두고, 붉은 색으로 표시된 <slot>을 정의했다. 이 위치에 정보를 전달할 부모 컴포넌트가 컴포넌트 내부에 콘텐츠로 작성한 내용이 전달된다.
<template>
<div id="app">
<speech-box :headerText="A.header" :footerText="A.footer">
<div>
<p>
{{A.message}}
</p>
</div>
</speech-box>
<speech-box class="sanders" :headerText="B.header" :footerText="B.footer">
<div>
<p class="sanders-content">
{{B.message}}
</p>
</div>
</speech-box>
</div>
</template>
<script>
import SpeechBox from './components/SpeechBox.vue'
export default {
name: 'app',
components : { SpeechBox },
data : function() {
return {
A : {
header : "오바마 대통령 고별 연설문",
footer : "2017.01.10 - 시카고",
message : `저의 동료 국민 여러분 ...(생략)`
},
B : {
header : "버니샌더스 경선 패배 연설문",
footer : "2016.07.25-필라델피아 웰스파고",
message : `감사합니다. ...(생략)`
}
};
}
}
</script>
src/App.vue의 내용이다. 붉은 색으로 표시한 부분이 자식 컴포넌트의 <slot></slot> 부분에 전달될 내용이다. 위처럼 컴포넌트 내부에 콘텐츠로 작성하여 전달이 가능한 것이다.
또한 콧수염 표현식으로 데이터를 전달할 수 있고, class도 전달할 수 있다.
슬롯에 이름을 부여한 명명된 슬롯(named slot)을 사용하면 컴포넌트에 여러 개의 슬롯을 작성할 수 있다.
<template>
<div id="pagewrap">
<header>
<slot name="header"></slot>
</header>
<aside id="sidebar">
<slot name="sidebar"></slot>
</aside>
<section id="content">
<slot name="content"></slot>
</section>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
src/components/NamedSlot.vue 파일의 내용이다. 각 slot에 name을 부여한 것을 볼 수 있다.
명명된 슬롯은 자식 컴포넌트에서 name만 부여하면 된다.
<template>
<div id="app">
<layout>
<h1 slot="header">헤더 영역</h1>
<div slot="sidebar">
<ul class="menu">
<li v-for="sidebar in sidebars" :key="sidebar.menu">
<a v-bind:href="sidebar.link">{{sidebar.menu}}</a>
</li>
</ul>
</div>
<div slot="content">
<h2>컨텐트 영역</h2>
<p>김수한무 거북이와 두루미</p>
<p>김수한무 거북이와 두루미</p>
<p>김수한무 거북이와 두루미</p>
<p>김수한무 거북이와 두루미</p>
</div>
<p slot="footer">Footer text</p>
</layout>
</div>
</template>
<script>
import Layout from './components/NamedSlot.vue';
export default {
data() {
return {
sidebars : [
{ menu : "Home", link : "#" },
{ menu : "About", link : "#" },
{ menu : "Contact", link : "#" },
{ menu : "Vue.js", link : "#" }
]
}
},
components : { Layout }
}
</script>
src/AppNamed.vue의 내용이다. NamedSlot.vue를 Layout이란 이름으로 import하여 <layout></layout> 태그를 만들어 사용하는 것을 볼 수 있다.
명명된 슬롯같은 경우는 부모 컴포넌트에서 slot 특성(attribute)를 이용해 지정한다.
범위 슬롯(Scoped Slot)은 자식 컴포넌트에서 부모 컴포넌트로 속성을 전달하여 부모 컴포넌트 측에서 출력할 내용을 커스터마이징 하도록 고안.
<template>
<div class="child">
X : <input type="text" v-model="x" /><br />
Y : <input type="text" v-model="y" /><br />
<slot name="type1" :cx="x" :cy="y"></slot>
<slot name="type2" :cx="x" :cy="y"></slot>
</div>
</template>
<script>
export default {
data() {
return { x:4, y:5 };
}
}
</script>
src/components/ScopedSlot.vue 파일의 내용이다. 자식 컴포넌트로써 자체적으로 x, y데이터를 가지고 있다. 그리고 slot에 :cx와 :cy라는 이름으로 바인딩하였다.
x와 y값은 v-model로 사용자가 입력할 때마다 양방향으로 데이터가 업데이트되도록 바인딩 되어 있으므로 :cx와 :cy값 또한 그에 맞게 변화한다.
<template>
<div class="parent">
<child>
<template slot="type1" scope="p1">
<div>{{p1.cx }} + {{p1.cy}} =
{{ parseInt(p1.cx) + parseInt(p1.cy) }}</div>
</template>
<template slot="type2" scope="p2">
<div>{{p2.cx }} 더하기 {{p2.cy}} 는
{{ parseInt(p2.cx) + parseInt(p2.cy) }}입니다.</div>
</template>
</child>
</div>
</template>
<script>
import Child from './components/ScopedSlot.vue'
export default {
components : { Child }
}
</script>
src/AppScoped.vue파일의 내용이다. slot attribute를 지정한 태그에 scope를 지정한 것을 확인할 수 있다. scope에 선언한 이름대로 자식 컴포넌트로부터 속성을 전달받고, 해당 템플릿 내에서만 사용이 가능하다.
자식 컴포넌트에서 :cx, :cy로 바인딩 한 데이터를 가져와 부모 컴포넌트에서 직접 다루는 모습을 확인할 수 있다.
이 범위 슬롯을 이용해서 자식 컴포넌트를 통해 출력할 내용을 부모 컴포넌트 측에서 직접 조작하는 용도로 사용한다.
화면의 동일한 위치에 여러 컴포넌트를 표현해야 할 때 사용한다.
<component> 요소를 템플릿에 작성하고 v-bind디렉티브를 이용해 is 특성 값으로 어떤 컴포넌트를 그 위치에 나타낼지 결정한다.
<template>
<div>
<div class="header">
<h1 class="headerText">(주) OpenSG</h1>
<nav>
<ul>
<li>
<a href="#" @click="changeMenu('home')">Home</a>
</li>
<li>
<a href="#" @click="changeMenu('about')">About</a>
</li>
<li>
<a href="#" @click="changeMenu('contact')">Contact</a>
</li>
</ul>
</nav>
</div>
<div class="container">
<keep-alive include="about,home">
<component v-bind:is="currentView"></component>
</keep-alive>
</div>
</div>
</template>
<script>
import Home from './components/Home.vue';
import About from './components/About.vue';
import Contact from './components/Contact.vue';
export default {
name: 'App',
components : { Home, About, Contact },
data() {
return { currentView : 'home' }
},
methods : {
changeMenu(view) {
this.currentView = view;
}
}
}
</script>
App.vue 파일의 내용이다. <component> 요소에 v-bind:is로 currentView라는 data값을 바인딩했다. @click이벤트의 발생시마다 currentView의 값을 변경하여 <component>요소에 들어가는 컴포넌트 내용이 동적으로 변경됨을 알 수 있다.
기본적으로 매번 실행되기 때문에, 정적 콘텐츠 등을 가져올 때는 캐시 해 두는 편이 좋다.
캐싱을 위해서 <keep-alive>로 감싸주었다.
특정 컴포넌트만 캐싱하거나 캐싱하고 싶지 않다면 exclude, include 특성으로 컴포넌트들을 콤마로 구분하여 나열하면 된다.
include에 포함된 컴포넌트들은 캐싱의 대상이 되고,
exclude에 포함된 컴포넌트들은 캐싱 대상에서 제외된다.
나열하는 것은 지정된 이름이고, 이것은 자식 컴포넌트에서 export시 정의한 name 속성의 값과 동일하다.
문자 그대로 템플릿에서 자기 자신을 호출하는 컴포넌트를 의미한다.
반드시 name옵션을 지정해야 한다.
<template>
<ul>
<li v-for="s in subs" v-bind:class="s.type" :key="s.name">
{{s.name}}
<tree :subs="s.subs"></tree>
</li>
</ul>
</template>
<script>
export default {
name : 'tree',
props : [ 'subs' ]
}
</script>
src/components/Tree.vue의 내용이다. name옵션을 지정한 것을 확인할 수 있고,
자기 자신 안에서 다시 <tree> 요소를 만들어 사용하고 있는 것을 볼 수 있다.
전달받은 데이터 subs안의 s내용 중 다시 subs가 있는 경우에 재귀적으로 그려지며, 더 이상 subs가 없을 때까지 반복된다.
다음 예제 코드를 참고하면 된다.
<template>
<div>
<h1>About</h1>
<h3>{{ (new Date()).toTimeString() }}</h3>
<h4>조직도</h4>
<tree :subs="orgcharts"></tree>
</div>
</template>
<script>
import Tree from './Tree.vue';
export default {
name : "about",
components : { Tree },
data : function() {
return {
orgcharts : [
{
name : "(주) OpenSG", type:"company",
subs : [
{ name: "SI 사업부", type:"division",
subs : [
{ name: "SI 1팀", type:"team" },
{ name: "SI 2팀", type:"team" }
]
},
{ name: "BI 사업부", type:"division",
subs : [
{ name: "BI 1팀", type:"team" },
{ name: "BI 2팀", type:"team" },
{ name: "BI 3팀", type:"team" }
]
},
{ name: "솔루션 사업부", type:"division",
subs : [
{ name: "ESM팀", type:"team" },
{ name: "MTS팀", type:"team" },
{ name: "ASF팀", type:"team" }
]
},
{ name: "총무팀", type:"team" },
{ name: "인사팀", type:"team" }
]
}
]
}
}
}
</script>
axios 라이브러리는 자동으로 설치되고 참조되지 않아 반드시 추가적으로 설치해야 한다.
vue create projectname
cd projectname
yarn add axios 혹은 npm install --save axios
혹은 <script> 태그를 이용해 CDN방식으로 직접 참조할 수도 있다.
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
브라우저 기본 설정 보안 정책 중 SOP(Same Origin Policy)가 있다.
HTML문서에서 다른 외부 서버와 통신하려는 경우 SOP 보안 정책으로 인해 크로스 오리진으로부터 데이터를 로드할 수 없다.
해결 방법으로는 다음과 같은 것들이 있다.
컨슈머 서버(Consumer Server) 측에 프록시 요소 생성 \
서비스 제공자(Service Provider) 측에서 CORS(Cross Origin Resources Sharing) 기능을 제공 \
서비스 제공자(Service Provider) 측에서 JSONP(JSON Padding) 기능을 제공
외부 서비스가 CORS나 JSONP를 제공한다면 프론트엔드에서는 아무 조치를 취하지 않아도 되지만 제공하지 않는 경우 컨슈머 서버에 프록시 요소를 생성해서 컨슈머를 거쳐 요청이 전달되도록 해야 한다.
따라서 웹 개발 기술로 HTTP Proxy를 만들어 사용해야 하는데, JSP, PHP등으로도 만들 수 있고 Tomcat이나 JBOSS등의 WAS들도 몇 가지 설정을 해주면 프록시 서버로 이용 가능하다
Vue CLI가 생성하는 프로젝트 템플릿 코드에서는 약간의 설정 파일만 작성하면 웹팩 개발 서버를 이용해 프록시 서버 기능을 곧바로 이용할 수 있다.
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
ws: true,
changeOrigin: true,
pathRewrite: {
'^/api' : ''
}
}
}
}
}
vue.config.js 파일의 내용이다.
이제 개발 서버에 /api/contacts 로 요청하면 http://localhost:3000/contacts로 요청을 전달한다. /api 경로로의 요청을 target으로 전달하는 것이다.
axios API 호출 후 리턴되는 객체는 Promise객체이므로 then과 catch를 체이닝할 수 있다.
[저수준 API]
axios(config)
axios(url, config)
[각 메소드별 별칭]
axios.get(url[, config])
axios.delete(url[, config])
axios.post(url[, data, [config]])
axios.put(url[, data, [config]])
axios.head(url[, config])
axios.options(url[, config])
[예제]
axios({
method : 'GET',
url : '/api/contacts',
params : { pageno : 1, pagesize:5 }
})
.then((response) => {
console.log(response);
this.result = response.data;
})
.catch((ex)=> {
console.log("ERROR!!!! : ", ex);
})
Promise 객체의 then 메서드 내부에서 함수 파라미터인 response는 다음과 같은 정보를 갖고 있다.
axios를 이용해 파일 업로드 기능을 구현하기 위해서는 input type=”file” 필드를 직접 참조해야 한다.
<input type="file" ref="photofile" name="photo"/>
ref 옵션을 이용해서 이 필드를 직접 참조하는 것이다.
changePhoto : function() {
var data = new FormData();
var file = this.$refs.photofile.files[0];
data.append('photo', file);
this.$axios.post('/api/contacts/' +this.no + '/photo', data)
.then((response) => {
this.result = response.data;
})
.catch((ex) => {
console.log('updatePhoto failed', ex);
});
}
vue 인스턴스의 methods에 정의되는 내용이다. FormData 객체를 생성하고 파일 필드를 직접 참조하여 객체에 추가한 뒤에 서버로 보내면 된다.
this가 의미하는 것은 뷰 인스턴스이며 .$refs로 ref옵션을 걸어둔 요소들을 가져올 수 있다.
위에서 사용한 params 옵션 외에도 다양한 옵션이 있다.
더 많은 옵션을 확인하려면 참고 : https://xn--xy1bk56a.run/axios/guide/api.html#%EA%B5%AC%EC%84%B1-%EC%98%B5%EC%85%98
Vue 인스턴스 내부에서 axios를 이용하기 위해 Vue.prototype에 axios를 추가하여 간단하게 이용할 수 있다. (import 하지 않고도 사용할 수 있다)
import Vue from 'vue'
import App from './AppAxiosTest.vue'
import axios from 'axios'
Vue.prototype.$axios = axios;
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')
Vue의 프로토타입에 $axios라는 이름으로 import된 axios를 등록했다.
이제 vue 인스턴스에서는 다음과 같이 사용할 수 있게 되었다. (페이지에서 import하지 않고)
methods : {
fetchContacts : function() {
this.$axios({
method : 'GET',
url : '/api/contacts',
params : { pageno : 1, pagesize:5 }
})
.then((response) => {
console.log(response);
this.result = response.data;
})
.catch((ex)=> {
console.log("ERROR!!!! : ", ex);
})
},
}
this.$axios를 사용하면 프로토타입으로부터 등록된 axios를 가져와서 import하지 않고 위와 같이 사용할 수 있게 되었다.
axios를 사용하면서 then()을 처리할 때는 ES2015의 화살표 함수를 사용할 것을 권장한다.
데이터를 수신한 후에 Vue 인스턴스 내부의 데이터를 변경해야 하는 경우가 많은데, 데이터 옵션을 액세스하기 위해서는 this 객체가 Vue인스턴스를 참조할 수 있어야 하기 때문이다.
(ES2015 문서에서 다루듯이, 화살표 함수에서의 this는 함수를 둘러싸고 있는 영역의 this를 그대로 따른다. https://docs.google.com/document/d/1WlU3Vde2uTao7RmbHgJkgjRcdRh1Sj4ETE0qyI1upmw/edit?usp=sharing 문서의 화살표 함수 참고)
then()내부에서 화살표함수를 사용하지 않으면 this가 Vue 인스턴스를 참조하지 않아 밖에서 별도의 변수에 this를 할당하고 Closure 방식으로 접근해야 하는 불편함이 있다.
Vuex는 상태 관리 라이브러리다.
상태 전용 전역 객체를 만들어서 관리하거나, 하나의 이벤트 버스로 모두 관리하는 방식으로 상태를 관리할 수도 있지만 아래와 같은 이점을 모두 얻기 어렵다.
(규모가 작은 애플리케이션이라면 이벤트 버스로 관리해도 충분하다)
이러한 이유로 Vuex를 사용한다.
Vuex는 애플리케이션 내부의 모든 컴포넌트들이 공유하는 중앙 집중화된 상태 정보 저장소 역할을 하여 상태 변경을 투명하게 할 수 있다. 각 컴포넌트가 공유하는 상태 데이터를 전역에서 저장소(store)객체를 통해서 관리한다.
따라서 부모-자식 컴포넌트간 props로 속성을 계속해서 전달하지 않아도 되고, 상태 데이터 변경을 위해 부모컴포넌트로 이벤트 발생을 시키지 않아도 된다.
컴포넌트에 저장소(store)객체의 데이터와 메서드를 로컬 컴포넌트의 계산형 속성이나 메서드에 연결하기만 하면 된다.
공식 문서의 데이터 흐름도다.
화살표가 한 방향으로만 흘러 다니는 것을 볼 수 있다. 그래서 “단방향 데이터 흐름”이라는 용어를 사용한다. 전체적인 처리 흐름은 다음과 같다.
컴포넌트가 액션(Action)을 일으킨다.
액션에서는 외부 API를 호출한 뒤 그 결과를 변이(Mutation)를 일으킨다. (외부 API가 없으면 생략)
변이에서는 액션의 결과를 받아 상태(State)를 변경한다. 이 단계는 추적이 가능하기 때문에 DevTools와 같은 도구를 이용하면 상태 변경 내역을 모두 확인할 수 있다.
변이에 의해 변경된 상태는 다시 컴포넌트에 바인딩되어 화면을 갱신한다.
점선으로 표시된 부분이 Vuex 저장소(Store)객체 영역이다. 상태/변이/액션을 관리한다.
일반적인 전역 객체와 달리 저장소 상태를 직접 변경하지 않고 반드시 변이(mutation)를 통해서만 변경한다.
변이의 목적은 상태의 변경이므로 관련이 없는 작업은 변이 내부에서 수행하지 않도록 한다.
또한 변이는 동기적인 작업이므로 비동기 처리는 변이를 통해서 수행하지 않는다. (비동기 처리시 정상 동작 할 수는 있겠지만, 추적 관리가 불가능하다)
상태 변화와 관련이 없는 작업은 액션(Action)을 정의해서 실행하여 비즈니스 로직이 실행되도록 한다. 그 후 액션의 처리 결과는 변이를 호출할 때 전달하여 상태(state)를 변경한다.
yarn add vuex 혹은 npm install --save vuex
*Vue CLI 도구의 vue add vuex 명령어로도 설치할 수 있지만, 이미 코드가 작성된 상태에서 vuex를 추가할 때는 npm install이나 yarn add해주는 것을 권장한다.
*npm install이 오류 나는 경우 node_modules 삭제하고 npm install이나 yarn install 명령어를 실행해서 의존성 패키지를 다시 다운로드 하면 문제를 해결할 수 있다.
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
todolist : [
{ id:1, todo : "영화보기", done:false },
{ id:2, todo : "주말 산책", done:true },
{ id:3, todo : "ES6 학습", done:false },
{ id:4, todo : "잠실 야구장", done:false },
]
},
mutations: {
[Constant.ADD_TODO] : (state, payload) => {
if (payload.todo !== "") {
state.todolist.push(
{ id:new Date().getTime(), todo: payload.todo, done:false });
}
},
[Constant.DONE_TOGGLE] : (state, payload) => {
var index = state.todolist.findIndex((item)=>item.id === payload.id);
state.todolist[index].done = !state.todolist[index].done;
},
[Constant.DELETE_TODO] : (state, payload) => {
var index = state.todolist.findIndex((item)=>item.id === payload.id);
state.todolist.splice(index,1);
}
}
})
export default store;
src/store/index.js의 내용이다. Vue.use(Vuex)는 Vuex를 전역에서 사용할 수 있도록 한다. mutation(변이)객체의 메서드들은 첫 번째 인자가 상태(state)고, 두 번째 인자가 payload이다. payload는 변이에서 필요로 하는 데이터다.
import Vue from 'vue'
import TodoList from './components/TodoList.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(TodoList)
}).$mount('#app')
src/main.js 파일의 내용이다. vue 인스턴스를 생성할 때 store객체를 전달한다. 이것과 더불어 index.js에서 Vue.use(Vuex)했기 때문에 모든 컴포넌트에서 store 객체를 this.$store로 접근할 수 있다.
<script type="text/javascript">
import Constant from '../Constant'
export default {
name: 'List',
computed : {
todolist() {
return this.$store.state.todolist;
}
},
methods : {
checked : function(done) {
if(done) return { checked:true };
else return { checked:false };
},
doneToggle : function(id) {
this.$store.commit(Constant.DONE_TOGGLE, {id:id})
},
deleteToggle : function(id) {
this.$store.commit(Constant.DELETE_TODO, {id:id})
}
}
}
</script>
직접 접근이 가능하므로 스토어에서 관리중인 데이터 this.$store.state.todolist를 읽기 전용으로 가져오는 것을 확인할 수 있다.
화면에서 일어나는 이벤트를 받아 변이를 일으키기 위해 this.$store.commit을 이용한다. 인자로 변이의 이름과 payload를 전달하는 것을 확인할 수 있다.
vuex는 mapState, mapMutations 와 같은 바인딩 헬퍼 메서드도 제공한다.
<template>
<ul id="todolist">
<li v-for="a in todolist" :key="a.id" :class="checked(a.done)"
@click="doneToggle({ id: a.id })">
<span>{{ a.todo }}</span>
<span v-if="a.done"> (완료)</span>
<span class="close" @click.stop="deleteTodo({id:a.id})">×</span>
</li>
</ul>
</template>
<script type="text/javascript">
import Constant from '../Constant'
import { mapState, mapMutations } from 'vuex'
export default {
name: 'List',
computed : mapState(['todolist']),
methods : {
checked : function(done) {
if(done) return { checked:true };
else return { checked:false };
},
...mapMutations([
Constant.DELETE_TODO,
Constant.DONE_TOGGLE
])
}
}
</script>
계산형 속성에 mapState를 사용해서 직접 작성하지 않도록 바뀐 것을 확인할 수 있다. 주의할 점은 저장소 상태의 이름과 동일한 이름으로 바인딩된다는 것이다.
다른 이름으로 바인딩하고 싶다면 다음과 같이 직접 작성할 수도 있다.
computed : mapState({
todolist2 : (state)=> state.todolist
})
mapMutations 헬퍼 메서드는 변이를 동일한 이름의 메서드에 자동으로 연결한다.
변이를 일으키지 않는 메서드가 함께 존재할 때는 mapMutations 헬퍼메서드가 만들어낸 객체와 변이를 일으키지 않는 메서드를 포함하는 객체를 병합하기 위해 전개 연산자(...)를 이용할 수 있다.
주의할 점은 컴포넌트에서 mapMutations 헬퍼 메서드를 이용해 변이를 메서드에 바인딩한 경우 호출할 때 변이의 인자 형식을 따라야 한다는 것이다.
위 예제에서 store에 정의된 인자 형식을 따르는 것을 확인할 수 있다.
만약 컴포넌트의 메서드 이름을 변이의 이름과 다르게 사용하고 싶다면 다음과 같이 객체 구조로 변이 명과 연결하는 메서드 이름을 지정할 수 있다.
methods : {
checked : function(done) {
if(done) return { checked:true };
else return { checked:false };
},
...mapMutations({
removeTodo : Constant.DELETE_TODO,
toggleDone : Constant.DONE_TOGGLE
})
}
메서드 이름을 지정해서 사용하면 해당 이름으로 바인딩해주면 된다.
게터는 저장소 수준의 계산형 속성이라고 말할 수 있다. 왜 사용하는가? 코드 중복을 줄이고 재사용성 높이려고...
게터에 대한 헬퍼 메서드가 mapGetters이다.
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
import _ from 'lodash';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
currentRegion : "all",
countries : [
{ no:1, name : "미국", capital : "워싱턴DC", region:"america" },
{ no:2, name : "프랑스", capital : "파리", region:"europe" },
{ no:3, name : "영국", capital : "런던", region:"europe" }
]
},
getters : {
countriesByRegion(state) {
if (state.currentRegion == "all") {
return state.countries;
} else {
return state.countries.filter(c => c.region==state.currentRegion);
}
},
regions(state) {
var temp = state.countries.map((c)=>c.region);
temp = _.uniq(temp);
temp.splice(0,0, "all");
return temp;
},
currentRegion(state) {
return state.currentRegion;
}
},
mutations: {
[Constant.CHANGE_REGION] : (state, payload) => {
state.currentRegion = payload.region;
}
}
})
export default store;
src/store/index.js의 내용이다. 저장소에서 게터 메서드를 정의하였다.
<template>
<div>
<button class="region" v-for="region in regions" :key="region"
v-bind:class="isSelected(region)"
@click="changeRegion({region:region})">
{{region}}
</button>
</div>
</template>
<script>
import Constant from '../Constant'
import { mapGetters, mapMutations } from 'vuex'
export default {
name : "RegionButtons",
computed : mapGetters([
'regions', 'currentRegion'
]),
// computed : {
// regions() {
// return this.$store.getters.regions;
// },
// currentRegion() {
// return this.$store.getters.currentRegion;
// }
// },
methods : {
isSelected(region) {
if (region == this.currentRegion) return { selected: true };
else return { selected:false }
},
...mapMutations([
Constant.CHANGE_REGION
])
}
}
</script>
src/components/RegionButtons.vue 컴포넌트에서 작성한 내용이다. 주석처리 된 부분은 헬퍼 메서드를 사용하지 않고 this.$store.getters를 이용해 가져온 것이고, 주석처리 되지 않은 부분에서 헬퍼 메서드 mapGetters를 통해 저장소로부터 정보를 가져온 것을 볼 수 있다.
앞서 언급했듯이 변이는 동기 처리를 위한 것이다. 비동기 처리를 위해서 기능적으로 분리한 것이 Action이다.
import Vue from 'vue';
import Vuex from 'vuex';
import Constant from '../Constant';
Vue.use(Vuex);
const store = new Vuex.Store({
state : {
todolist : [
{ id:1, todo : "영화보기", done:false },
{ id:2, todo : "주말 산책", done:true }
]
},
mutations: {
[Constant.ADD_TODO] : (state, payload) => {
if (payload.todo !== "") {
state.todolist.push(
{ id:new Date().getTime(), todo: payload.todo, done:false });
}
},
[Constant.DONE_TOGGLE] : (state, payload) => {
var index = state.todolist.findIndex((item)=>item.id === payload.id);
state.todolist[index].done = !state.todolist[index].done;
},
[Constant.DELETE_TODO] : (state, payload) => {
var index = state.todolist.findIndex((item)=>item.id === payload.id);
state.todolist.splice(index,1);
}
},
actions : {
[Constant.ADD_TODO] : (store, payload) => {
store.commit(Constant.ADD_TODO, payload);
},
[Constant.DELETE_TODO] : (store, payload) => {
store.commit(Constant.DELETE_TODO, payload);
},
[Constant.DONE_TOGGLE] : (store, payload) => {
store.commit(Constant.DONE_TOGGLE, payload);
}
}
})
export default store;
src/store/index.js의 내용이다. 액션의 메서드들은 첫 번째 인자로 store, 두 번째 인자로 payload를 받는 것을 알 수 있다.
액션에서 변이를 일으키기 위해 store객체의 commit을 이용하는 것도 확인할 수 있다.
또한 store객체를 전달받았기 때문에 당연히 state, getters, mutations, actions를 모두 이용할 수 있다.
<template>
<div>
<input class="input" type="text" id="task" v-model.trim="todo"
placeholder="입력 후 엔터!" v-on:keyup.enter="addTodo">
<span class="addbutton" v-on:click="addTodo">추 가</span>
</div>
</template>
<script type="text/javascript">
import Constant from '../Constant'
export default {
name : 'input-todo',
data : function() {
return { todo : "" }
},
methods : {
addTodo : function() {
this.$store.dispatch(Constant.ADD_TODO, { todo: this.todo });
this.todo = "";
}
}
}
</script>
src/components/InputTodo.vue의 내용이다. 컴포넌트에서 위와 같이 this.$store.dispatch 하면 액션을 호출할 수 있다.
만일 메서드가 동일한 이름의 액션을 호출한다면 mapActions 헬퍼 메서드를 이용할 수 있다.
export default {
name: 'List',
computed : mapState(['todolist']),
methods : {
checked : function(done) {
if(done) return { checked:true };
else return { checked:false };
},
...mapActions([ Constant.DELETE_TODO, Constant.DONE_TOGGLE ])
}
}
</script>
이벤트가 발생한 메서드와 action 이름이 동일하다면 위와 같이 쓸 수 있다.
지금까지는 store/index.js 파일 하나로 모든 상태,변이,액션,게터를 모아 관리했다.
대규모 애플리케이션을 개발할 때는 하나의 파일로 관리하기가 쉽지 않아 분리하는 편이 좋다.
import 구문으로 저장소를 참조할 때 store 디렉터리까지만 지정하면 그 내부의 index.js를 우선적으로 로드한다.
이 파일에서 다시 import 구문을 이용해 상태, 변이, 액션, 게터를 로드하는 구조로 변경하여 사용할 수 있다.
각각 js 파일들을 state.js, actions.js, mutations.js와 같이 생성하고, 원래 작성했던 방식에서 해당 부분만 가져가 객체를 만든 뒤에 export default 하면 된다.
그 후에 index.js에서는 각각 덩어리를 import해서 new Vuex.Store() 할 때 인자로 넘겨주면 된다.
간단하므로 예제생략
여러 개의 모듈로 나누어 저장소를 관리할 수도 있다.
모듈은 자체적인 상태, 변이, 액션, 게터를 가지는 저장소의 하위 객체다.
애플리케이션 전체에서 상태를 한 객체로 관리하는 것은 부담스러운 일이다.
Store수준에서 하위에 여러 모듈을 나눠 계층 구조로 관리할 수 있다.
const module1 = {
state : { ... },
mutations : { ... },
actions : { ... },
getters : { ... }
}
const module2 = {
state : { ... },
mutations : { ... },
actions : { ... }
}
const store = new Vuex.Store({
.....
modules : {
m1 : module1,
m2 : module2
}
})
모듈 객체를 정의하고, new Vuex.Store()로 스토어를 생성할 때 modules 속성으로 지정하면 된다.
저장소와 모듈은 모두 각각의 상태,변이,게터,액션을 가진다.
모든 모듈과 저장소에서 actions와 mutations는 공유하지만 state나 getters는 공유되지 않는다.
따라서 액션에서 상위 저장소의 상태(state)를 이용해야 하는 경우에는 store객체를 통해 저장소의 루트 상태에 접근해야 한다.
this.$store.rootGetters
this.$store.rootState
지금까지 사용했던 commit, dispatch, getters, state와 같이 Store 객체에 rootGetters와 rootState 속성이 존재하므로 사용할 수 있다.
하지만 반대로 루트 저장소의 액션에서는 모듈의 상태에 접근할 수 없다. 그러므로 상태 데이터가 전역 수준인지, 특정 모듈에서만 사용하는지 등 구조에 유의해야 한다.
vue-router는 vue.js의 공식 라우터 라이브러리다. Vue.js의 핵심 요소와 깊이 통합되어 SPA를 손쉽게 만들 수 있도록 도와준다. 제공하는 기능은 다음과 같다.
HTML에서는 <router-link> 태그를 통해 라우터를 통해 이동(변경)하는 하이퍼링크를 지정할 수 있으며, 이를 통해 보여질 내용을 <router-view>태그 위치에 렌더링 할 수 있다.
JavaScript에서는 우선 보여질 Component를 작성하고, VueRouter 객체에 속성으로 name혹은 path와 함께 해당 컴포넌트를 매핑시켜준다. 이 VueRouter 객체는 Vue인스턴스에 주입 되어 라우터를 사용할 수 있게 된다.
HTML
<script src="https://unpkg.com/vue/dist/vue.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 네비게이션을 위해 router-link 컴포넌트를 사용합니다. -->
<!-- 구체적인 속성은 `to` prop을 이용합니다. -->
<!-- 기본적으로 `<router-link>`는 `<a>` 태그로 렌더링됩니다.-->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 라우트 아울렛 -->
<!-- 현재 라우트에 맞는 컴포넌트가 렌더링됩니다. -->
<router-view></router-view>
</div>
JavaScript
// 0. 모듈 시스템 (예: vue-cli)을 이용하고 있다면, Vue와 Vue 라우터를 import 하세요
// 그리고 `Vue.use(VueRouter)`를 호출하세요
// 1. 라우트 컴포넌트를 정의하세요.
// 아래 내용들은 다른 파일로부터 가져올 수 있습니다.
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 라우트를 정의하세요.
// Each route should map to a component. The "component" can
// 각 라우트는 반드시 컴포넌트와 매핑되어야 합니다.
// "component"는 `Vue.extend()`를 통해 만들어진
// 실제 컴포넌트 생성자이거나 컴포넌트 옵션 객체입니다.
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. `routes` 옵션과 함께 router 인스턴스를 만드세요.
// 추가 옵션을 여기서 전달해야합니다.
// 지금은 간단하게 유지하겠습니다.
const router = new VueRouter({
routes // `routes: routes`의 줄임
})
// 4. 루트 인스턴스를 만들고 mount 하세요.
// router와 router 옵션을 전체 앱에 주입합니다.
const app = new Vue({
router
}).$mount('#app')
주어진 패턴을 가진 라우트를 동일한 컴포넌트에 매핑해야하는 경우 동적 세그먼트를 사용할 수 있다. 동적 세그먼트는 콜론으로 표시되며, 라우트가 일치하면 동적 세그먼트의 값은 모든 컴포넌트에서 this.$route.params로 표시된다.
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const router = new VueRouter({
routes: [
// 동적 세그먼트는 콜론으로 시작합니다.
{ path: '/user/:id', component: User }
]
})
동일한 라우트에 여러 동적 세그먼트를 가질 수 있으며, $route.params
의 해당 필드에 매핑된다.
패턴 | 일치하는 패스 | $route.params |
/user/:username | /user/evan | { username: 'evan' }
|
/user/:username/post/:post_id | /user/evan/post/123 | { username: 'evan', post_id: '123' }
|
매개 변수와 함께 라우트를 사용할 때 주의해야할 점은 동일한 컴포넌트 인스턴스가 재사용된다는 것이다. 이는 컴포넌트 라이플 사이클 훅이 호출되지 않음을 의미한다. 따라서 동일한 컴포넌트의 params 변경 사항에 반응하려면 $route 객체를 보면 된다.
const User = {
template: '...',
watch: {
'$route' (to, from) {
// 경로 변경에 반응하여...
}
}
}
<router-view>에 렌더링 될 컴포넌트 안에서 다시 <router-view>가 나타나는 구조의 경우 일반적으로 URL의 세그먼트가 중첩 된 컴포넌트의 특정 구조와 일치한다.
이럴 때는 VueRouter 생성자의 옵션 config안 routes에서 children속성을 사용하면 된다.
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User,
children: [
{
// /user/:id/profile 과 일치 할 때
// UserProfile은 User의 <router-view> 내에 렌더링 됩니다.
path: 'profile',
component: UserProfile
},
{
// /user/:id/posts 과 일치 할 때
// UserPosts가 User의 <router-view> 내에 렌더링 됩니다.
path: 'posts',
component: UserPosts
}
]
}
]
})
<router-link>를 사용하여 선언적 네비게이션용 anchor 태그를 사용하는 방법 말고도, 인스턴스 메소드 router.push를 사용하여 다른 URL로 이동할 수 있다. 이는 <router-link>를 클릭 할 때 내부적으로 호출되는 메소드이므로 <router-link :to="...">를 클릭하면 router.push(...)를 호출하는 것과 같다.
// 리터럴 string
router.push('home')
// object
router.push({ path: 'home' })
// 이름을 가지는 라우트
router.push({ name: 'user', params: { userId: 123 }})
// 쿼리와 함께 사용, 결과는 /register?plan=private 입니다.
router.push({ path: 'register', query: { plan: 'private' }})
*params를 사용하는 경우, 이름을 가지는 라우트 (name)로 사용해야 한다. path만으로 매핑하는 경우 정상작동을 하지 않는다고 함. 아래 “이름을 가지는 라우트” 참고
router.push와 같은 역할을 하지만, 새로운 히스토리 항목에 추가하지 않고 탐색한다.
이 메소드는 window.history.go(n)와 비슷하게 히스토리 스택에서 앞으로 또는 뒤로 이동하는 단계를 나타내는 하나의 정수를 매개 변수로 사용한다.
// 한 단계 앞으로 갑니다. history.forward()와 같습니다. history.forward()와 같습니다.
router.go(1)
// 한 단계 뒤로 갑니다. history.back()와 같습니다.
router.go(-1)
// 3 단계 앞으로 갑니다.
router.go(3)
// 지정한 만큼의 기록이 없으면 자동으로 실패 합니다.
router.go(-100)
router.go(100)
라우트에 연결하거나 탐색을 수행 할 때 이름이 있는 라우트를 사용하는 것이 더 편리할 때가 있다. 또한 params를 사용하려면 이름이 있는 라우터를 사용해야 한다.
const router = new VueRouter({
routes: [
{
path: '/user/:userId',
name: 'user',
component: User
}
]
})
여러 개의 뷰를 중첩하지 않고 동시에 표시해야 하는 경우 뷰에 이름을 준다. 이름이 없는 router-view는 이름으로 default가 주어진다.
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
동일한 라우트에 여러 개의 컴포넌트를 사용하는 경우에는 component가 아닌 components 속성을 주어야 한다.
const router = new VueRouter({
routes: [
{
path: '/',
components: {
default: Foo,
a: Bar,
b: Baz
}
}
]
})
라우터에서도 리디렉션을 지정할 수 있다. (예시에서 번호는 각각 방식을 설명하기 위함)
const router = new VueRouter({
routes: [
1. { path: '/a', redirect: '/b' }
2. { path: '/a', redirect: { name: 'foo' }}
3. { path: '/a', redirect: to => {
// 함수는 인수로 대상 라우트를 받습니다.
// 여기서 path/location 반환합니다.
}}
]
})
별칭을 사용하면 구성의 중첩 구조에 의해 제약을 받는 대신 UI 구조를 임의의 URL에 매핑 할 수 있다.
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
컴포넌트에서 $route를 사용하면 특정 URL에서만 사용할 수 있는 컴포넌트의 유연성을 제한하는 라우트와 강한 결합을 만든다.
컴포넌트와 라우터 속성을 분리하려면 다음과 같이 할 수 있다.
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true },
]
})
동적 세그먼트의 값을 props로 전달받아 사용하는 것을 볼 수 있다.
어디서나 컴포넌트를 사용할 수 있으므로 컴포넌트 재사용 및 테스트하기가 더 쉽다.
props를 true로 설정하면 route.params가 컴포넌트 props로 설정되는 것이다.
props는 또한 위에 설명한 boolean 모드 외에 객체 모드, 함수 모드로 사용할 수 있다.
[객체 모드]
const router = new VueRouter({
routes: [
{ path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
]
})
props가 객체일 때는 props가 있는 그대로 설정된다. (props가 정적일 때 유용하게 사용할 수 있다)
[함수 모드]
const router = new VueRouter({
routes: [
{ path: '/search', component: SearchUser, props: (route) => ({ query: route.query.q }) }
]
})
props가 함수 형태일 때는 props를 반환하는 함수를 만들면 된다.
router.beforeEach를 사용하여 보호하기 이전에 전역 등록을 할 수 있다.
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
네비게이션이 트리거될 때마다 가드가 작성 순서에 따라 호출되기 전의 모든 경우에 발생합니다. 가드는 비동기식으로 실행 될 수 있으며 네비게이션은 모든 훅이 해결되기 전까지 보류 중 으로 간주됩니다.
모든 가드 기능은 세 가지 전달인자를 받습니다.
to: 라우트
: 대상 Route 객체 로 이동합니다.from: 라우트
: 현재 라우트로 오기전 라우트 입니다.next: 함수
: 이 함수는 훅을 해결하기 위해 호출 되어야 합니다. 액션은 next
에 제공된 전달인자에 달려 있습니다.next()
: 파이프라인의 다음 훅으로 이동하십시오. 훅이 없는 경우 네비게이션은 승인됩니다.next(false)
: 현재 네비게이션을 중단합니다. 브라우저 URL이 변경되면(사용자 또는 뒤로 버튼을 통해 수동으로 변경됨) from
경로의 URL로 재설정됩니다.next('/')
또는 next({ path: '/' })
: 다른 위치로 리디렉션합니다. 현재 네비게이션이 중단되고 새 네비게이션이 시작됩니다.next(error)
: (2.4.0 이후 추가) next
에 전달된 인자가 Error
의 인스턴스이면 탐색이 중단되고 router.onError()
를 이용해 등록 된 콜백에 에러가 전달됩니다.**항상 next
함수를 호출하십시오. 그렇지 않으면 훅이 절대 불러지지 않습니다.
beforeEnter 가드를 라우트의 설정 객체에 직접 정의 할 수 있다.
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})
beforeRouteEnter 와 beforeRouteLeave를 사용하여 라우트 컴포넌트(라우터 설정으로 전달되는 컴포넌트) 안에 라우트 네비게이션 가드를 직접 정의 할 수 있다.
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 이 컴포넌트를 렌더링하는 라우트 앞에 호출됩니다.
// 이 가드가 호출 될 때 아직 생성되지 않았기 때문에
// `this` 컴포넌트 인스턴스에 접근 할 수 없습니다!
},
beforeRouteLeave (to, from, next) {
// 이 컴포넌트를 렌더링하는 라우트가 이전으로 네비게이션 될 때 호출됩니다.
// `this` 컴포넌트 인스턴스에 접근 할 수 있습니다.
}
}
beforeRouteEnter 가드는 네비게이션이 확인되기 전에 가드가 호출되어서 새로운 엔트리 컴포넌트가 아직 생성되지 않았기 때문에 this에 접근하지 못한다.
그러나 콜백을 next에 전달하여 인스턴스에 액세스 할 수 있다. 네비게이션이 확인되고 컴포넌트 인스턴스가 콜백에 전달인자로 전달 될 때 콜백이 호출된다.
beforeRouteEnter (to, from, next) {
next(vm => {
// `vm`을 통한 컴포넌트 인스턴스 접근
})
}
*그 외 HTML5 히스토리 모드, 기타 고급 사용법은 공식 문서를 참고하도록 한다.
공식 문서 참고 : https://router.vuejs.org/kr/guide/
Vue.js에서는 트랜지션 css 클래스들을 손쉽게 적용할 수 있도록 트랜지션 래퍼 컴포넌트를 지원한다. 모든 요소, 컴포넌트, router-view를 감싸주는 것 만으로도 효과를 손쉽게 지정할 수 있다. 기능에 큰 영향을 주지 않기 때문에 참고하여 사용하도록 한다.
공식 문서 참고 : https://kr.vuejs.org/v2/guide/transitions.html
Vue CLI는 크게 3가지 구성요소로 이루어져 있다.
설치 커맨드 : npm install -g @vue/cli
어디서나 실행할 수 있도록 npm을 이용해 전역으로 설치하며 터미널에서 vue 명령어를 통해 다음과 같은 기능을 사용할 수 있음
프로젝트가 생성될 때 개발 의존성으로 설치되는 구성요소. 웹팩과 웹팩 개발 서버 기반으로 작성되어있다.
Vue CLI로 생성된 프로젝트 추가적인 기능을 제공하는 npm 패키지. CLI 도구를 이용해 프로젝트를 생성할 때 추가할 플러그인 선택 가능하고, 생성된 이후에도 vue add 명령어를 이용해 추가할 수도 있다. 대표적인 플러그인은 다음과 같은 것이 있다.
터미널 : vue create [프로젝트명]
프리셋 설정은 자유롭게, 참고 : https://cli.vuejs.org/guide/plugins-and-presets.html#presets
vue create로 생성된 프로젝트의 구조, 각 디렉터리와 파일의 내용은 다음과 같다.
개발자가 작성하는 소스 코드를 배치하는 디렉터리
배포 버전을 빌드할 때 필요한 파일. 웹팩을 사용해 이 파일을 로드한 뒤 설정을 추가하여 빌드 버전을 만듦.
이 위치에 index.html이 존재한다. 뼈대라고 볼 수 있다.
앱 개발과 배포에 필요한 npm 패키지들이 저장되는 디렉터리
작성한 앱 코드를 빌드하여 만든 배포 버전을 저장하는 디렉터리. Vue 컴포넌트들은 모두 js 파일로 트랜스파일되어 몇 개의 js 파일로 번들링되고, 난독화되어 저장.
각 컴포넌트의 스타일 정보들도 하나의 css로 번들링되어 저장.
index.html은 public디렉터리의 html 파일을 로딩하여 .js, .css파일들의 경로를 지정한 <link><script> 태그를 추가하여 dist 디렉터리에 저장한다.
package.json 파일의 “scripts”필드를 보면 스크립트 명이 보이는데, 이것은 복잡한 명령을 npm run [스크립트명] 과 같이 간단히 실행할 수 있도록 도와준다.
내용을 보면 vue-cli-service를 이용해서 실행하는 것을 볼 수 있다.
vue-cli-service는 전역 수준(--global)으로 설치하지 않았기 때문에 직접 터미널에서 실행하려면 node_modules 경로로 이동해서 사용해야 한다.
그러나 스크립트를 이용하면 프로젝트 의존성으로 설치된 플러그인도 직접 호출할 수 있다.
ex) npm run serve
vue-cli-service는 명령어와 옵션을 이용할 수 있다. serve시 브라우저 바로 띄우기, 포트 설정 등이 가능하다.
자세한 내용 공식문서 참고 : https://cli.vuejs.org/guide/cli-service.html#vue-cli-service-build
Vue CLI의 내부는 웹팩이라는 모듈 번들러 도구를 이용해 만들어져 있다. vue의 CLI서비스는 모두 캡슐화가 되어 있기 때문에 내부의 웹팩에 대해 웹팩 설정 파일을 이용해 직접 설정할 수 없다. 대신 웹팩 설정을 위해 vue.config.js라는 파일을 프로젝트 내부에 작성한다.
vue.config.js는 Vue CLI로 생성한 프로젝트에서 웹팩에 대한 기본 구성 설정을 추가하거나 변경할 수 있는 기능을 제공하는 파일이다. 이 파일을 이용하면 기본 설정은 vue-cli-service에 내장된 복잡한 설정을 그대로 둔 채로 추가적인 설정을 할 수 있다.
자세한 내용 : https://cli.vuejs.org/config/
*책에서 axios를 이용한 HTTP통신을 할 때 웹팩 개발 서버의 proxy설정을 위해 해당 파일 작성을 한다.
설명생략. 참고 : https://cli.vuejs.org/guide/creating-a-project.html#using-the-gui
검색해도 많이 나온다. 쓸 일이 많지 않겠지?
vue 인스턴스의 computed, methods 속성 안에 만든 함수에서 this는 vue 인스턴스 자체를 가리키지만, 해당 함수 안에서 콜백 함수를 다시 호출하는 등의 경우 this가 가리키는 대상이 변경될 수 있다(Global Object 혹은 Windows 등).
ECMAScript 6가 제공하는 화살표 함수로 작성하는 경우 이 함수 내부에서 this는 Vue인스턴스를 가리키지 않고 전역 객체(Global Object 혹은 Windows)를 가리킨다. 따라서 vue인스턴스의 methods에서 화살표 함수를 사용하는 것을 지양해야 한다.
컴포넌트 탭에서 설명한 것처럼 컴포넌트를 이용해 애플리케이션을 작성하는 경우 data옵션은 반드시 객체를 리턴하는 함수 형태로 작성해야한다.
브라우저는 DOM 구문 분석을 먼저 수행하고, 그 후에 Vue 컴포넌트가 렌더링되므로 구문 분석 오류가 발생할 수 있어 템플릿을 사용할 때에는 is 특성(속성)을 사용한다.
참고로, .vue 확장자를 사용하는 단일 파일 컴포넌트는 굳이 사용하지 않아도 된다.
<template>요소 안에서 루트 요소는 반드시 하나여야 한다.
단위 테스트
mocha.js (https://programmingsummaries.tistory.com/383)
Jest (https://www.daleseo.com/jest-basic/)
서버 사이드 렌더링
Nuxt.js (https://ko.nuxtjs.org/)