Vue.js 3에서 Custom Directive(사용자 정의 디렉티브)는 DOM 요소에 직접적으로 특정 동작을 추가하거나 제어하기 위해 사용하는 기능입니다. Vue.js에는 기본 디렉티브(v-model, v-if, v-for 등)가 있지만, 상황에 따라 특정 요구 사항에 맞는 동작을 구현해야 할 때 Custom Directive를 활용할 수 있습니다. v-접두어를 사용하여 만들 수 있으며 반복적인 작업을 v-접두어를 통해 줄일 수 있습니다.
const app = createApp({})
// 모든 컴포넌트에서 v-focus를 사용할 수 있도록 합니다.
app.directive('focus', {
/* ... */
})
<script setup>
// 템플릿에서 v-focus로 활성화 가능
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
또한 Directive는 여러 훅을 사용할 수 있습니다. 모든 훅은 필수로 입력해야 하는것이 아닌, 선택적으로 사용할 수 있습니다.
const myDirective = {
// 바인딩된 엘리먼트의 속성 또는
// 이벤트 리스너가 적용되기 전에 호출됩니다.
created(el, binding, vnode, prevVnode) {
// 인자에 대한 자세한 내용은 아래를 참고.
},
// 엘리먼트가 DOM에 삽입되기 직전에 호출됩니다.
beforeMount(el, binding, vnode, prevVnode) {},
// 바인딩된 엘리먼트의 부모 컴포넌트 및
// 모든 자식 컴포넌트의 mounted 이후에 호출됩니다.
mounted(el, binding, vnode, prevVnode) {},
// 부모 컴포넌트의 updated 전에 호출됩니다.
beforeUpdate(el, binding, vnode, prevVnode) {},
// 바인딩된 엘리먼트의 부모 컴포넌트 및
// 모든 자식 컴포넌트의 updated 이후에 호출됩니다.
updated(el, binding, vnode, prevVnode) {},
// 부모 컴포넌트의 beforeUnmount 이후에 호출됩니다.
beforeUnmount(el, binding, vnode, prevVnode) {},
// 부모 컴포넌트의 unmounted 전에 호출됩니다.
unmounted(el, binding, vnode, prevVnode) {}
}
디렉티브 훅에는 다음 인자가 전달됩니다:
- el: 디렉티브가 바인딩된 엘리먼트입니다. DOM을 직접 조작하는 데 사용할 수 있습니다.
- binding: 다음 속성을 포함하는 객체입니다.
- value: 디렉티브에 전달된 값입니다. 예를 들어 v-my-directive="1 + 1"에서 - value는 2입니다.
- oldValue: 이것은 beforeUpdate 및 updated에서만 사용할 수 있습니다. 값이 변경되었는지 여부에 관계없이 사용 가능합니다.
- arg: 디렉티브에 전달된 인자(있는 경우). 예를 들어 v-my-directive:foo에서 인자는 "foo"입니다.
- modifiers: 수식어가 있는 경우 수식어를 포함하는 객체입니다. 예를 들어 v-my-directive.foo.bar에서 수식어 객체는 { foo: true, bar: true }입니다.
- instance: 디렉티브가 사용되는 컴포넌트의 인스턴스입니다.
- dir: 디렉티브를 정의하는 객체
- vnode: 바인딩된 엘리먼트를 나타내는 기본 VNode.
- prevVnode: 이전 렌더링에서 바인딩된 엘리먼트를 나타내는 VNode입니다. beforeUpdate 및 updated 훅에서만 사용할 수 있습니다.
저는 반복적으로 Interaction Observer를 사용할 것이기 때문에 전역적으로 사용하겠습니다.
// lazyLoad.js
const lazyLoad = {
mounted(el) {
const setBackgroundImage = () => {
if(el.className.includes('lazy')) {
//dataset에 들어있는 url을 바인딩
el.style.backgroundImage = `url(${el.dataset.url})`;
}
}
const setObserver = () => {
const obOptions = {
root: null,
rootMargin: '0px 0px 100px 0px',
threshold: 0.5, // 관찰 대상의 50% 이상이 보일 때 콜백 호출
};
const observer = new IntersectionObserver((iter)=>{
iter.forEach((item) => {
if(item.isIntersecting) {
setBackgroundImage();
observer.unobserve(el);
}
});
}, obOptions)
observer.observe(el);
}
// IntersectionObserver가 없는 브라우저에서 호환을 위한 처리.
window.IntersectionObserver ? setObserver() : setBackgroundImage()
}
}
export default lazyLoad;
전역 Directive는 main.js에서 등록할 수 있습니다.
import { createApp } from "vue"
import laztLoad from '@/directives/lazyLoad.ts'
//main.ts
const app = createApp();
//directive 메서드를 사용하여 등록
//v-lazy와 같은 형태로 사용.
app.directive('lazy', lazyLoad);
지역적으로 사용하고 싶다면 script setup에서 v 접두사로 시작하는 모든 camelCase 변수를 커스텀 디렉티브로 사용할 수 있습니다.
<script setup>
//v 접두사로 시작하여 디렉티브 설정
const vLazy = {
mounted(el) {
const setBackgroundImage = () => {
if(el.className.includes('lazy')) {
//dataset에 들어있는 url을 바인딩
el.style.backgroundImage = `url(${el.dataset.url})`;
}
}
const setObserver = () => {
const obOptions = {
root: null,
rootMargin: '0px 0px 100px 0px',
threshold: 0.5, // 관찰 대상의 50% 이상이 보일 때 콜백 호출
};
const observer = new IntersectionObserver((iter)=>{
iter.forEach((item) => {
if(item.isIntersecting) {
setBackgroundImage();
observer.unobserve(el);
}
});
}, obOptions)
observer.observe(el);
}
// IntersectionObserver가 없는 브라우저에서 호환을 위한 처리.
window.IntersectionObserver ? setObserver() : setBackgroundImage()
}
}
</script>
<template>
<div v-lazy :data-src="`@/assets/someImage.webp`" />
</template>