지금까지 본 $emit
을 사용하고 'v-on
으로 듣는 방법 이외에도 Vue 인스턴스는 또 다른 이벤트 인터페이스 사용 방법을 가지고 있다.
$on(eventName, eventHandler)
을 이용한 이벤트 청취$once(eventName, eventHandler)
을 이용한 단발성 이벤트 청취$off(eventName, eventHandler)
을 이용한 이벤트 청취 중단Limit
// datepicker를 input에 한 번 연결함
// DOM에 직접 연결됨
mounted: function () {
// Pikaday는 서드파티 라이브러리
this.picker = new Pikaday({ // 인스턴스 변수
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 컴포넌트를 destroy하기 직전에
// datepicker를 destroy함
beforeDestroy: function () {
this.picker.destroy()
}
프로그래밍적 리스너를 이용하면 위 두가지 이슈를 모두 해결할 수 있다.
mounted: function () {
// mounted에서만 쓸 수 있게 로컬 변수로 선언
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
// mounted 내에서 destroy 처리
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
위 방법을 사용하면 Pikaday를 다양한 엘리먼트에 사용할 수 있고 각각의 새로운 인스턴스는 사용된 후 destroy 하기 전에 스스로 picker를 destroy 하게 된다.
mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})
this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}
앞서 말했듯, 위와 같이 Pikaday 여러 개를 컴포넌트 안에서 만들어 사용할 수 있다.
컴포넌트는 재귀적으로 템플릿 안에서 호출될 수 있지만 name 옵션을 이용해서만 호출될 수 있다.
name: 'unique-name-of-my-component'
Vue.component를 이용해 컴포넌트를 전역으로 등록하는 경우, 전역ID는 자동으로 컴포넌트의 name 옵션의 값으로 설정된다.
Vue.component('unique-name-of-my-component', {
// ...
})
주의하지 않으면 재귀 컴포넌트는 무한 루프를 발생 시킬 수 있다.
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
위와 같은 컴포넌트는 "max stack size exceeded(최대 스택 사이즈가 초과되었습니다)" 오류를 발생시키므로 재귀 호출이 조건부인지 확인한다.
이와 같은 이유로 이 방법은 추천하지 않는다.
Finder나 File Explorer 같은 파일 디렉토리 트리를 만드는 경우를 생각했을 때,
<!-- 부모 컴포넌트 -->
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>
<!-- 자식 컴포넌트 -->
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>
역설적이게도 렌더링 트리에서 컴포넌트끼리 서로의 자식이자 부모임을 알 수 있다.
(닭과 계란 중 뭐가 먼저인가 같은 상황)
컴포넌트를 글로벌하게 등록하는 경우 이 역설은 자동으로 해결된다.
단, 모듈 시스템(Webpack 또는 Browserify)을 통해 컴포넌트를 require 혹은 import를 시도하는 경우 아래와 같은 오류가 발생한다.
Failed to mount component: template or render function not defined.
A를 부르려니 B가 필요하고 B를 부르려니 A가 필요한, 서로가 서로에 의해 정의되어지는 상황이 생긴다.
즉, 서로 정의되기 전에 정의가 될 수 없는 무한 루프에 빠지게 된다.
이 문제를 해결하려면 모듈 시스템에 "A는 결국 B를 필요로 하지만 B를 먼저 해결할 필요는 없다"고 말할 수 있는 지점을 제공해야 한다.
beforeCreate 라이플 사이클 훅을 이용해 호출되기를 기다렸다가 자식 컴포넌트를 등록하는 방법으로 처리하면 된다.
beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}
components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}