컴포넌트(Component)
조합하여 화면을 구성할 수 있는 블록을 뜻한다.

화면 영역을 각각 Header, Content, Footer, Aside, Main으로 구분한 것이다.
이를 아래와 같은 트리 형태로 나타낼 수 있다.

트리 구조로 컴포넌트를 살펴보면 컴포넌트가 어떻게 구성되어있는지 더 알아보기 쉽다.
Root는 화면 전체 영역을 의미한다.
화면 전체를 Header, Content, Footer 세 개의 컴포넌트로 분할하였다. 이후 Content 컴포넌트에서도 Aside, Main 두 개의 컴포넌트로 분할한다.
Content를 부모 컴포넌트 또는 상위 컴포넌트라 하고, Aside, Main을 자식 컴포넌트 또는 하위 컴포넌트라 말한다. 이는 Root와 Header, Content, Footer 컴포넌트 사이의 관계에서도 마찬가지이다.
전역 컴포넌트
전역 컴포넌트는 여러 인스턴스에서 공통으로 사용할 수 있는 컴포넌트이다.
Vue.component('컴포넌트 이름', 컴포넌트 내용);
컴포넌트 이름은 template 속성에서 사용할 HTML 사용자 정의 태그 이름이다.
컴포넌트 내용 부분에는 객체가 들어간다. 실제 화면의 HTML 요소로 변환될 때 표시될 속성들을 작성한다.
컴포넌트 내용에는 template, data, methods 등의 인스턴스 옵션 속성을 사용할 수 있다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<global-component></global-component>
</div>
<div id="app2">
<global-component></global-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('global-component', {
template: '<p>전역 컴포넌트</p>'
});
var vm = new Vue({
el: "#app"
});
</script>
</body>
</html>
컴포넌트 이름이 태그 이름이 된다.
위 예제에서는 컴포넌트 이름을 global-component라고 하여 <global-component> 태그를 사용해 컴포넌트를 HTML 요소로 변환시킬 수 있다.
<global-component> 태그 부분이 global-component 컴포넌트 내용으로 치환된다.
즉, <global-component></global-component>가 <p>전역 컴포넌트</p>로 치환된다.

전역 컴포넌트라고 해서 DOM의 어느 부분이든 상관없이 모두 사용할 수 있는 것은 아니다. 뷰 인스턴스 내에서만 사용할 수 있다.
뷰 인스턴스에서 el 속성을 '#app'만 지정하였기 때문에 해당 id 값을 가진 태그 내에서만 사용할 수 있다. app2라는 id 값을 가진 <div> 태그 내에서는 global-component 컴포넌트 내용이 출력되지 않았다.
뷰 인스턴스를 하나 더 생성하고 해당 인스턴스의 el 속성을 '#app2'로 지정한다면 <div id="app2"> 태그 내에서도 컴포넌트 내용이 나타날 것이다.

Vue.js devtools를 보면 Root 컴포넌트 안에 global-component 컴포넌트가 등록된 것을 확인할 수 있다.
이때 Root는 상위 컴포넌트 또는 부모 컴포넌트라 부르고, global-component는 하위 컴포넌트 또는 자식 컴포넌트로 위치한다.
전역 컴포넌트 처리 과정을 살펴보면 다음과 같다.
뷰 라이브러리 파일이 로딩되고, 뷰 생성자로 컴포넌트를 등록한다(Vue.component()).
옵션 속성을 포함한 인스턴스 객체가 생성되고 특정 화면 요소에 생성된 인스턴스를 부착한다.
부착이 되면 인스턴스 내용이 변환된다. 등록된 컴포넌트 내용도 변환된다.
이렇게 변환된 화면을 사용자가 보게 된다.
근데 실제 서비스에서는 전역 컴포넌트를 등록할 일이 거의 없다.
예를 들어 웹팩 같은 빌드 시스템을 사용하고 전역 등록한 컴포넌트가 있을 때, 해당 컴포넌트를 사용하지 않더라도 최종 빌드에는 들어간다. 따라서 사용자가 내려받아야 하는 불필요한 자바스크립트가 생긴다.
지역 컴포넌트
지역 컴포넌트는 뷰 인스턴스 내에 components 속성을 정의하고 속성 값에 객체(컴포넌트 이름, 컴포넌트 내용)를 넣으면 된다.
new Vue({
components: {
'컴포넌트 이름': 컴포넌트 내용
}
});
컴포넌트 내용도 전역 컴포넌트 생성 때와 마찬가지로 객체 값이 들어간다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<local-component></local-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
components: {
'local-component': {
template: '<p>지역 컴포넌트</p>'
}
}
});
</script>
</body>
</html>

전역 컴포넌트와 달리 인스턴스 생성자 내 components 속성을 주어 생성하였다.
전역 컴포넌트와 지역 컴포넌트의 차이
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app1">
<global-component></global-component>
<local-component></local-component>
</div>
<hr />
<div id="app2">
<global-component></global-component>
<local-component></local-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('global-component', {
template: '<p>전역 컴포넌트</p>'
});
new Vue({
el: '#app1',
components: {
'local-component': {
template: '<p>지역 컴포넌트</p>'
}
}
});
new Vue({
el: '#app2'
});
</script>
</body>
</html>


전역 컴포넌트로 생성한 global-component는 <div id="app1">과 <div id="app2"> 두 태그 내에서 모두 사용할 수 있다.
그러나 지역 컴포넌트로 생성한 local-component는 <div id="app1"> 태그 내에서만 유효하기 때문에 <div id="app2">에서는 컴포넌트 내용이 출력되지 않았다. 따라서 지역 컴포넌트를 사용하려는 인스턴스마다 components 속성을 사용하여 등록해주어야 한다.
지역 컴포넌트는 싱글 페이 컴포넌트 체계로 갔을 때 특정 컴포넌트 하단에 어떤 컴포넌트가 등록되었는지 컴포넌트 속성으로 알 수 있다. 그러나 전역 컴포넌트는 실제 구현 시 대부분 플러그인이나 라이브러리 형태로, 전역으로 사용해야 되는 컴포넌트만 나타나게 된다.
컴포넌트 통신
각각의 컴포넌트는 독립적인 유효 범위를 가진다.
따라서 A 컴포넌트의 데이터를 B 컴포넌트에서 마음대로 사용할 수 없다.
다른 컴포넌트로 데이터를 전달하기 위해서는 뷰에서 정의해놓은 데이터 전달 방식에 따라 해야 한다. 데이터를 공유하며 관리하기 위해 props 속성과 event를 사용한다.
기존 MVC 패턴 사용 시 (n방향 통신) 뷰가 정의해놓은 방식과는 다르게 컴포넌트들이 유기적으로 데이터를 주고받았다. 이는 데이터의 흐름 방향이 정해져 있지 않기 때문에 문제가 발생하면 어디서 발생한 문제인지 찾기 쉽지 않다.
이러한 문제를 해결하기 위해 뷰에서는 컴포넌트 통신 규칙을 사용하게 되었다.
컴포넌트 통신 규칙을 사용하면 데이터의 흐름은 위에서 아래 또는 아래에서 위로만 흐르기 때문에 문제 발생 시 흐름을 추적하여 문제를 찾는 게 더 편리해졌다.
컴포넌트 통신 방식
상위-하위 컴포넌트 간 데이터 전달
상위에서 하위로 데이터 전달 시 props 속성을 전달하여 데이터를 내려준다.
하위에서 상위로는 데이터를 전달하지 않고 이벤트를 올린다.
// props 속성 정의
Vue.component('child-component', {
props: ['props 속성 이름'],
});
// 상위 컴포넌트 HTML 코드
<child-component v-bind:props 속성 이름="상위 컴포넌트의 data 속성"></child-component>
// 이벤트 발생
this.$emit('이벤트명');
// 이벤트 수신
<child-component v-on:하위 컴포넌트에서 발생한 이벤트명="상위 컴포넌트의 메서드명"></child-component>
먼저 상위에서 하위로 props 속성을 사용해 데이터를 전달하는 코드를 살펴본다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<msg-data v-bind:propsdata="message"></msg-data>
<num-data v-bind:propsdata="num"></num-data>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var msgData = {
template: '<h1>{{propsdata}}</h1>',
props: ['propsdata']
}
var numData = {
template: '<div>{{propsdata}}</div>',
props: ['propsdata']
}
new Vue({
el: '#app',
components: {
'msg-data': msgData,
'num-data': numData
},
data: {
message: 'Hello',
num: 10
}
})
</script>
</body>
</html>
컴포넌트 내용은 코드 가독성을 위해 변수에 저장하여 작성했다.
컴포넌트 내용에 props 속성을 추가한다. props 속성 값은 HTML 태그에서 사용할 props 속성 이름을 작성한다.
HTML 부분에서 v-bind 디렉티브를 사용하여 props로 전달받은 데이터를 가져올 수 있다. props 속성 이름을 propsdata로 하였기 때문에 v-bind:propsdata를 사용하여 data를 가져온다.


Vue.js devtools에서 살펴보면 각각의 컴포넌트 내에 props > props 속성 이름 : 데이터 값이 뜬다.
데이터 값을 변경하면 뷰의 반응성 덕분에 바로 변경된 값으로 화면에 출력된다.
하위에서 상위로는 이벤트가 발생한다.
데이터 전달이라고 명명하기보다는 이벤트 발생이라 하는데, 속성 값을 통해 데이터를 전달할 수도 있다.
그러나 하위에서 상위로 데이터를 전달하는 것은 뷰의 단방향 데이터 흐름에 어긋나는 구현 방법이기 때문에 뷰 공식 사이트에서는 다루지 않는다.
우선 간단하게 어떻게 이벤트가 발생하는지 살펴본다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<event-button></event-button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var eventButton = {
template: '<button v-on:click="passEvent">click me</button>',
methods: {
passEvent: function() {
this.$emit('pass');
}
}
}
new Vue({
el: '#app',
components: {
'event-button': eventButton
}
});
</script>
</body>
</html>
컴포넌트 내용에 methods 속성을 넣고 passEvent 메서드를 생성하였다.
passEvent 메서드에서는 this.$emit()을 사용하여 이벤트를 발생시킨다. 인자 값은 이벤트명을 작성한다. 현재 이벤트명을 pass라고 설정하였다.
template 속성에 <button>을 정의한다. 해당 버튼은 v-on:click 디렉티브를 사용하여 클릭 시 passEvent가 발생한다.

click me라는 버튼을 클릭할 때마다 Vue.js devtools의 Component events란에서 이벤트가 발생하는 것을 확인할 수 있다.
위 예제는 단순히 이벤트를 발생시키기만 하는 예제이다.
이벤트를 활용하여 아래에서 위 방향으로 데이터 전달을 할 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{ num }}</p>
<add-num-event v-on:increase="addNumber"></add-num-event>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var addNumEvent = {
template: '<button v-on:click="incNumber">add</button>',
methods: {
incNumber: function () {
this.$emit('increase');
}
}
}
new Vue({
el: '#app',
components: {
'add-num-event': addNumEvent
},
methods: {
addNumber: function() {
this.num = this.num + 1;
}
},
data: {
num: 10
}
})
</script>
</body>
</html>
혼돈을 피하기 위해 함수 이름은 각각 다르게 정의하였다.
Root 컴포넌트의 내용에는 data인 num 값과 num 값을 1씩 증가시키는 addNumber 메서드를 생성하였다.
하위 컴포넌트인 add-num-event 컴포넌트에서 template에 <button>을 정의하고, 해당 버튼을 클릭하면 incNumber 이벤트가 발생한다. 위 예시로는 add-num-event 컴포넌트에서 Root 컴포넌트로 'increase'라는 이벤트를 보내는 것이다.
<add-num-event>에서 v-on 디렉티브를 사용하여 하위 컴포넌트의 이벤트인 increase를 받아온다.
해당 이벤트를 받아 상위 컴포넌트의 addNumber 메서드를 실행시킨다.
이렇게 함으로써 하위 컴포넌트에서 상위 컴포넌트의 data인 num 값을 변경시키는 이벤트를 발생시킨다.

add-num-event 컴포넌트에서 생성된 add 버튼을 누르면 Root 컴포넌트의 data인 num 값이 1씩 증가한다.
같은 레벨 컴포넌트 간 데이터 전달
같은 레벨 컴포넌트의 경우 컴포넌트끼리 바로 데이터 전달이 불가능하다.
결국은 하위 컴포넌트(B)에서 상위 컴포넌트(A)에 데이터를 전달한 뒤, 전달된 데이터를 다른 하위 컴포넌트(C)로 전달해주어야 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<app-header v-bind:propsdata="num"></app-header>
<app-content v-on:pass="deliverNum"></app-content>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var appHeader = {
template: '<div>header</div>',
props: ['propsdata']
}
var appContent = {
template: '<div>content <button v-on:click="passNum">pass</button></div>',
methods: {
passNum: function() {
this.$emit('pass', 10);
}
}
}
new Vue({
el: '#app',
components: {
'app-header': appHeader,
'app-content': appContent
},
data: {
num: 0
},
methods: {
deliverNum: function(val) {
this.num = val;
}
}
})
</script>
</body>
</html>
하위 컴포넌트(app-content)의 데이터인 num 10 값을 같은 레벨인 다른 하위 컴포넌트(app-header)로 옮긴다.
우선 app-content 컴포넌트에서 Root 컴포넌트로 이벤트를 발생시킨다.
이벤트 이름은 pass로 설정하고, 10이라는 값을 이벤트에 넣는다.
pass 버튼을 생성하여 해당 버튼을 클릭하면 passNum 메서드를 실행시켜 pass 이벤트가 발생하게 한다.
pass 이벤트 발생 시 Root 컴포넌트에서 deliverNum 메서드가 실행된다.
이벤트를 통해 가져온 데이터 값을 this.num에 저장한다.
v-on:pass="deliverNum"라고 작성하여 따로 괄호로 인자를 넣어주지 않았다. 하지만 function()에 매개변수를 명시하면 자동으로 이벤트 발생 시 넘어온 데이터를 인자로 가져온다.
따라서 이벤트 발생 시 가져온 데이터 값인 10을 this.num에 저장하는 것이다. (이때의 this는 Root 컨텐트의 data 옵션 속성 내 값을 가리킨다.)
이렇게 해서 하위 컴포넌트에서 상위 컴포넌트로 데이터 값을 전달해주었다.
이후 상위 컴포넌트인 Root에서 하위 컴포넌트인 app-header로 데이터를 전달해준다.
props 속성을 사용하여 Root 컴포넌트의 num이라고 하는 데이터 값을 가져온다.

pass 버튼 클릭 이전에는 app-header 컴포넌트의 propsdata가 0으로 되어있다.
pass 버튼을 클릭한다.

propsdata가 10으로 변경된 것을 확인할 수 있다.
관계없는 컴포넌트 간 데이터 전달
이벤트 버스(Event Bus)를 사용하여 상위-하위 또는 같은 레벨 관계가 아닌 컴포넌트들끼리도 데이터를 전달할 수 있다.
var eventBus = new Vue(); // 이벤트 버스를 위한 추가 인스턴스
methods: { // 이벤트를 보내는 컴포넌트
메서드명: function() {
eventBus.$emit('이벤트명', 데이터);
}
}
methods: { // 이벤트를 받는 컴포넌트
created: function() {
eventBus.$on('이벤트명', function(데이터) {
});
}
}
애플리케이션 로직을 담는 인스턴스와는 별개로 새로운 인스턴스를 생성하고 해당 인스턴스를 이용하여 이벤트를 보내고 받는다.
보내는 컴포넌트에서는 .$emit()을 구현하고, 받는 컴포넌트에서는 .$on()을 구현한다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<child-component></child-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var eventBus = new Vue();
Vue.component('child-component', {
template: '<div>하위 컴포넌트 영역 <button v-on:click="showLog">show</button></div>',
methods: {
showLog: function() {
eventBus.$emit('triggerEventBus', 100);
}
}
});
new Vue({
el: '#app',
created: function() {
eventBus.$on('triggerEventBus', function(value) {
console.log("이벤트 전달 받음. 값 : ", value);
})
}
});
</script>
</body>
</html>
이벤트 버스로 활용할 인스턴스를 생성하여 변수 eventBus에 담았다. 따라서 eventBus 변수를 사용해 새 인스턴스의 속성과 메서드에 접근한다.
하위 컴포넌트인 child-component에서 triggerEventBus 이벤트를 발생시킨다. 발생한 이벤트를 받는 쪽은 상위 컴포넌트인 Root 컴포넌트로 .$on()을 사용하여 triggerEventBus 이벤트를 수신한다.
이렇게 하여 props 속성을 이용하지 않고 컴포넌트 간 데이터 전달을 할 수 있다. 그러나 이 방법은 데이터를 어디서 어디로 보냈는지 관리가 되지 않는 문제가 있다.
컴포넌트 이름과 관련한 스타일 가이드
https://v2.vuejs.org/v2/style-guide/
Style Guide — Vue.js
Vue.js - The Progressive JavaScript Framework
v2.vuejs.org
[공식 문서] https://vuejs.org/v2/guide/components-registration.html
[도서] Do it! Vue.js 입문
[인프런] Vue.js 시작하기 - Age of Vue.js
컴포넌트(Component)
조합하여 화면을 구성할 수 있는 블록을 뜻한다.

화면 영역을 각각 Header, Content, Footer, Aside, Main으로 구분한 것이다.
이를 아래와 같은 트리 형태로 나타낼 수 있다.

트리 구조로 컴포넌트를 살펴보면 컴포넌트가 어떻게 구성되어있는지 더 알아보기 쉽다.
Root는 화면 전체 영역을 의미한다.
화면 전체를 Header, Content, Footer 세 개의 컴포넌트로 분할하였다. 이후 Content 컴포넌트에서도 Aside, Main 두 개의 컴포넌트로 분할한다.
Content를 부모 컴포넌트 또는 상위 컴포넌트라 하고, Aside, Main을 자식 컴포넌트 또는 하위 컴포넌트라 말한다. 이는 Root와 Header, Content, Footer 컴포넌트 사이의 관계에서도 마찬가지이다.
전역 컴포넌트
전역 컴포넌트는 여러 인스턴스에서 공통으로 사용할 수 있는 컴포넌트이다.
Vue.component('컴포넌트 이름', 컴포넌트 내용);
컴포넌트 이름은 template 속성에서 사용할 HTML 사용자 정의 태그 이름이다.
컴포넌트 내용 부분에는 객체가 들어간다. 실제 화면의 HTML 요소로 변환될 때 표시될 속성들을 작성한다.
컴포넌트 내용에는 template, data, methods 등의 인스턴스 옵션 속성을 사용할 수 있다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<global-component></global-component>
</div>
<div id="app2">
<global-component></global-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('global-component', {
template: '<p>전역 컴포넌트</p>'
});
var vm = new Vue({
el: "#app"
});
</script>
</body>
</html>
컴포넌트 이름이 태그 이름이 된다.
위 예제에서는 컴포넌트 이름을 global-component라고 하여 <global-component> 태그를 사용해 컴포넌트를 HTML 요소로 변환시킬 수 있다.
<global-component> 태그 부분이 global-component 컴포넌트 내용으로 치환된다.
즉, <global-component></global-component>가 <p>전역 컴포넌트</p>로 치환된다.

전역 컴포넌트라고 해서 DOM의 어느 부분이든 상관없이 모두 사용할 수 있는 것은 아니다. 뷰 인스턴스 내에서만 사용할 수 있다.
뷰 인스턴스에서 el 속성을 '#app'만 지정하였기 때문에 해당 id 값을 가진 태그 내에서만 사용할 수 있다. app2라는 id 값을 가진 <div> 태그 내에서는 global-component 컴포넌트 내용이 출력되지 않았다.
뷰 인스턴스를 하나 더 생성하고 해당 인스턴스의 el 속성을 '#app2'로 지정한다면 <div id="app2"> 태그 내에서도 컴포넌트 내용이 나타날 것이다.

Vue.js devtools를 보면 Root 컴포넌트 안에 global-component 컴포넌트가 등록된 것을 확인할 수 있다.
이때 Root는 상위 컴포넌트 또는 부모 컴포넌트라 부르고, global-component는 하위 컴포넌트 또는 자식 컴포넌트로 위치한다.
전역 컴포넌트 처리 과정을 살펴보면 다음과 같다.
뷰 라이브러리 파일이 로딩되고, 뷰 생성자로 컴포넌트를 등록한다(Vue.component()).
옵션 속성을 포함한 인스턴스 객체가 생성되고 특정 화면 요소에 생성된 인스턴스를 부착한다.
부착이 되면 인스턴스 내용이 변환된다. 등록된 컴포넌트 내용도 변환된다.
이렇게 변환된 화면을 사용자가 보게 된다.
근데 실제 서비스에서는 전역 컴포넌트를 등록할 일이 거의 없다.
예를 들어 웹팩 같은 빌드 시스템을 사용하고 전역 등록한 컴포넌트가 있을 때, 해당 컴포넌트를 사용하지 않더라도 최종 빌드에는 들어간다. 따라서 사용자가 내려받아야 하는 불필요한 자바스크립트가 생긴다.
지역 컴포넌트
지역 컴포넌트는 뷰 인스턴스 내에 components 속성을 정의하고 속성 값에 객체(컴포넌트 이름, 컴포넌트 내용)를 넣으면 된다.
new Vue({
components: {
'컴포넌트 이름': 컴포넌트 내용
}
});
컴포넌트 내용도 전역 컴포넌트 생성 때와 마찬가지로 객체 값이 들어간다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<local-component></local-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
components: {
'local-component': {
template: '<p>지역 컴포넌트</p>'
}
}
});
</script>
</body>
</html>

전역 컴포넌트와 달리 인스턴스 생성자 내 components 속성을 주어 생성하였다.
전역 컴포넌트와 지역 컴포넌트의 차이
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app1">
<global-component></global-component>
<local-component></local-component>
</div>
<hr />
<div id="app2">
<global-component></global-component>
<local-component></local-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
Vue.component('global-component', {
template: '<p>전역 컴포넌트</p>'
});
new Vue({
el: '#app1',
components: {
'local-component': {
template: '<p>지역 컴포넌트</p>'
}
}
});
new Vue({
el: '#app2'
});
</script>
</body>
</html>


전역 컴포넌트로 생성한 global-component는 <div id="app1">과 <div id="app2"> 두 태그 내에서 모두 사용할 수 있다.
그러나 지역 컴포넌트로 생성한 local-component는 <div id="app1"> 태그 내에서만 유효하기 때문에 <div id="app2">에서는 컴포넌트 내용이 출력되지 않았다. 따라서 지역 컴포넌트를 사용하려는 인스턴스마다 components 속성을 사용하여 등록해주어야 한다.
지역 컴포넌트는 싱글 페이 컴포넌트 체계로 갔을 때 특정 컴포넌트 하단에 어떤 컴포넌트가 등록되었는지 컴포넌트 속성으로 알 수 있다. 그러나 전역 컴포넌트는 실제 구현 시 대부분 플러그인이나 라이브러리 형태로, 전역으로 사용해야 되는 컴포넌트만 나타나게 된다.
컴포넌트 통신
각각의 컴포넌트는 독립적인 유효 범위를 가진다.
따라서 A 컴포넌트의 데이터를 B 컴포넌트에서 마음대로 사용할 수 없다.
다른 컴포넌트로 데이터를 전달하기 위해서는 뷰에서 정의해놓은 데이터 전달 방식에 따라 해야 한다. 데이터를 공유하며 관리하기 위해 props 속성과 event를 사용한다.
기존 MVC 패턴 사용 시 (n방향 통신) 뷰가 정의해놓은 방식과는 다르게 컴포넌트들이 유기적으로 데이터를 주고받았다. 이는 데이터의 흐름 방향이 정해져 있지 않기 때문에 문제가 발생하면 어디서 발생한 문제인지 찾기 쉽지 않다.
이러한 문제를 해결하기 위해 뷰에서는 컴포넌트 통신 규칙을 사용하게 되었다.
컴포넌트 통신 규칙을 사용하면 데이터의 흐름은 위에서 아래 또는 아래에서 위로만 흐르기 때문에 문제 발생 시 흐름을 추적하여 문제를 찾는 게 더 편리해졌다.
컴포넌트 통신 방식
상위-하위 컴포넌트 간 데이터 전달
상위에서 하위로 데이터 전달 시 props 속성을 전달하여 데이터를 내려준다.
하위에서 상위로는 데이터를 전달하지 않고 이벤트를 올린다.
// props 속성 정의
Vue.component('child-component', {
props: ['props 속성 이름'],
});
// 상위 컴포넌트 HTML 코드
<child-component v-bind:props 속성 이름="상위 컴포넌트의 data 속성"></child-component>
// 이벤트 발생
this.$emit('이벤트명');
// 이벤트 수신
<child-component v-on:하위 컴포넌트에서 발생한 이벤트명="상위 컴포넌트의 메서드명"></child-component>
먼저 상위에서 하위로 props 속성을 사용해 데이터를 전달하는 코드를 살펴본다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<msg-data v-bind:propsdata="message"></msg-data>
<num-data v-bind:propsdata="num"></num-data>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var msgData = {
template: '<h1>{{propsdata}}</h1>',
props: ['propsdata']
}
var numData = {
template: '<div>{{propsdata}}</div>',
props: ['propsdata']
}
new Vue({
el: '#app',
components: {
'msg-data': msgData,
'num-data': numData
},
data: {
message: 'Hello',
num: 10
}
})
</script>
</body>
</html>
컴포넌트 내용은 코드 가독성을 위해 변수에 저장하여 작성했다.
컴포넌트 내용에 props 속성을 추가한다. props 속성 값은 HTML 태그에서 사용할 props 속성 이름을 작성한다.
HTML 부분에서 v-bind 디렉티브를 사용하여 props로 전달받은 데이터를 가져올 수 있다. props 속성 이름을 propsdata로 하였기 때문에 v-bind:propsdata를 사용하여 data를 가져온다.


Vue.js devtools에서 살펴보면 각각의 컴포넌트 내에 props > props 속성 이름 : 데이터 값이 뜬다.
데이터 값을 변경하면 뷰의 반응성 덕분에 바로 변경된 값으로 화면에 출력된다.
하위에서 상위로는 이벤트가 발생한다.
데이터 전달이라고 명명하기보다는 이벤트 발생이라 하는데, 속성 값을 통해 데이터를 전달할 수도 있다.
그러나 하위에서 상위로 데이터를 전달하는 것은 뷰의 단방향 데이터 흐름에 어긋나는 구현 방법이기 때문에 뷰 공식 사이트에서는 다루지 않는다.
우선 간단하게 어떻게 이벤트가 발생하는지 살펴본다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<event-button></event-button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var eventButton = {
template: '<button v-on:click="passEvent">click me</button>',
methods: {
passEvent: function() {
this.$emit('pass');
}
}
}
new Vue({
el: '#app',
components: {
'event-button': eventButton
}
});
</script>
</body>
</html>
컴포넌트 내용에 methods 속성을 넣고 passEvent 메서드를 생성하였다.
passEvent 메서드에서는 this.$emit()을 사용하여 이벤트를 발생시킨다. 인자 값은 이벤트명을 작성한다. 현재 이벤트명을 pass라고 설정하였다.
template 속성에 <button>을 정의한다. 해당 버튼은 v-on:click 디렉티브를 사용하여 클릭 시 passEvent가 발생한다.

click me라는 버튼을 클릭할 때마다 Vue.js devtools의 Component events란에서 이벤트가 발생하는 것을 확인할 수 있다.
위 예제는 단순히 이벤트를 발생시키기만 하는 예제이다.
이벤트를 활용하여 아래에서 위 방향으로 데이터 전달을 할 수 있다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<p>{{ num }}</p>
<add-num-event v-on:increase="addNumber"></add-num-event>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var addNumEvent = {
template: '<button v-on:click="incNumber">add</button>',
methods: {
incNumber: function () {
this.$emit('increase');
}
}
}
new Vue({
el: '#app',
components: {
'add-num-event': addNumEvent
},
methods: {
addNumber: function() {
this.num = this.num + 1;
}
},
data: {
num: 10
}
})
</script>
</body>
</html>
혼돈을 피하기 위해 함수 이름은 각각 다르게 정의하였다.
Root 컴포넌트의 내용에는 data인 num 값과 num 값을 1씩 증가시키는 addNumber 메서드를 생성하였다.
하위 컴포넌트인 add-num-event 컴포넌트에서 template에 <button>을 정의하고, 해당 버튼을 클릭하면 incNumber 이벤트가 발생한다. 위 예시로는 add-num-event 컴포넌트에서 Root 컴포넌트로 'increase'라는 이벤트를 보내는 것이다.
<add-num-event>에서 v-on 디렉티브를 사용하여 하위 컴포넌트의 이벤트인 increase를 받아온다.
해당 이벤트를 받아 상위 컴포넌트의 addNumber 메서드를 실행시킨다.
이렇게 함으로써 하위 컴포넌트에서 상위 컴포넌트의 data인 num 값을 변경시키는 이벤트를 발생시킨다.

add-num-event 컴포넌트에서 생성된 add 버튼을 누르면 Root 컴포넌트의 data인 num 값이 1씩 증가한다.
같은 레벨 컴포넌트 간 데이터 전달
같은 레벨 컴포넌트의 경우 컴포넌트끼리 바로 데이터 전달이 불가능하다.
결국은 하위 컴포넌트(B)에서 상위 컴포넌트(A)에 데이터를 전달한 뒤, 전달된 데이터를 다른 하위 컴포넌트(C)로 전달해주어야 한다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<div id="app">
<app-header v-bind:propsdata="num"></app-header>
<app-content v-on:pass="deliverNum"></app-content>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var appHeader = {
template: '<div>header</div>',
props: ['propsdata']
}
var appContent = {
template: '<div>content <button v-on:click="passNum">pass</button></div>',
methods: {
passNum: function() {
this.$emit('pass', 10);
}
}
}
new Vue({
el: '#app',
components: {
'app-header': appHeader,
'app-content': appContent
},
data: {
num: 0
},
methods: {
deliverNum: function(val) {
this.num = val;
}
}
})
</script>
</body>
</html>
하위 컴포넌트(app-content)의 데이터인 num 10 값을 같은 레벨인 다른 하위 컴포넌트(app-header)로 옮긴다.
우선 app-content 컴포넌트에서 Root 컴포넌트로 이벤트를 발생시킨다.
이벤트 이름은 pass로 설정하고, 10이라는 값을 이벤트에 넣는다.
pass 버튼을 생성하여 해당 버튼을 클릭하면 passNum 메서드를 실행시켜 pass 이벤트가 발생하게 한다.
pass 이벤트 발생 시 Root 컴포넌트에서 deliverNum 메서드가 실행된다.
이벤트를 통해 가져온 데이터 값을 this.num에 저장한다.
v-on:pass="deliverNum"라고 작성하여 따로 괄호로 인자를 넣어주지 않았다. 하지만 function()에 매개변수를 명시하면 자동으로 이벤트 발생 시 넘어온 데이터를 인자로 가져온다.
따라서 이벤트 발생 시 가져온 데이터 값인 10을 this.num에 저장하는 것이다. (이때의 this는 Root 컨텐트의 data 옵션 속성 내 값을 가리킨다.)
이렇게 해서 하위 컴포넌트에서 상위 컴포넌트로 데이터 값을 전달해주었다.
이후 상위 컴포넌트인 Root에서 하위 컴포넌트인 app-header로 데이터를 전달해준다.
props 속성을 사용하여 Root 컴포넌트의 num이라고 하는 데이터 값을 가져온다.

pass 버튼 클릭 이전에는 app-header 컴포넌트의 propsdata가 0으로 되어있다.
pass 버튼을 클릭한다.

propsdata가 10으로 변경된 것을 확인할 수 있다.
관계없는 컴포넌트 간 데이터 전달
이벤트 버스(Event Bus)를 사용하여 상위-하위 또는 같은 레벨 관계가 아닌 컴포넌트들끼리도 데이터를 전달할 수 있다.
var eventBus = new Vue(); // 이벤트 버스를 위한 추가 인스턴스
methods: { // 이벤트를 보내는 컴포넌트
메서드명: function() {
eventBus.$emit('이벤트명', 데이터);
}
}
methods: { // 이벤트를 받는 컴포넌트
created: function() {
eventBus.$on('이벤트명', function(데이터) {
});
}
}
애플리케이션 로직을 담는 인스턴스와는 별개로 새로운 인스턴스를 생성하고 해당 인스턴스를 이용하여 이벤트를 보내고 받는다.
보내는 컴포넌트에서는 .$emit()을 구현하고, 받는 컴포넌트에서는 .$on()을 구현한다.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Vue.js</title>
</head>
<body>
<div id="app">
<child-component></child-component>
</div>
<br />
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
var eventBus = new Vue();
Vue.component('child-component', {
template: '<div>하위 컴포넌트 영역 <button v-on:click="showLog">show</button></div>',
methods: {
showLog: function() {
eventBus.$emit('triggerEventBus', 100);
}
}
});
new Vue({
el: '#app',
created: function() {
eventBus.$on('triggerEventBus', function(value) {
console.log("이벤트 전달 받음. 값 : ", value);
})
}
});
</script>
</body>
</html>
이벤트 버스로 활용할 인스턴스를 생성하여 변수 eventBus에 담았다. 따라서 eventBus 변수를 사용해 새 인스턴스의 속성과 메서드에 접근한다.
하위 컴포넌트인 child-component에서 triggerEventBus 이벤트를 발생시킨다. 발생한 이벤트를 받는 쪽은 상위 컴포넌트인 Root 컴포넌트로 .$on()을 사용하여 triggerEventBus 이벤트를 수신한다.
이렇게 하여 props 속성을 이용하지 않고 컴포넌트 간 데이터 전달을 할 수 있다. 그러나 이 방법은 데이터를 어디서 어디로 보냈는지 관리가 되지 않는 문제가 있다.
컴포넌트 이름과 관련한 스타일 가이드
https://v2.vuejs.org/v2/style-guide/
Style Guide — Vue.js
Vue.js - The Progressive JavaScript Framework
v2.vuejs.org
[공식 문서] https://vuejs.org/v2/guide/components-registration.html
[도서] Do it! Vue.js 입문
[인프런] Vue.js 시작하기 - Age of Vue.js