Vue는 항목(html tag, component)이 DOM에 삽입, 갱신 또는 제거 될 때 트랜지션 효과를 적용하는 다양한 방법을 제공한다.
Vue는 transition wrapper component인 <transition>
을 제공하여 다음과 같은 상황에서 모든 엘리먼트 또는 컴포넌트에 대한 Enter∙Leave 트랜지션을 추가 할 수 있다.
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#demo',
data: {
show: true
}
})
.fade-enter-active, .fade-leave-active {
transition: opacity .5s;
}
.fade-enter, .fade-leave-to { /* .fade-leave-active below version 2.1.8 */
opacity: 0;
}
Enter∙Leave 트렌지션에는 6가지 클래스가 적용된다.
각 클래스에는 트랜지션의 name 속성이 접두어로 붙으며 v-접두어는 이름없이 <transition>
엘리먼트를 사용할 때의 기본값이다.
v-enter
: enter의 시작 상태v-enter-active
: enter 활성화 상태, 진입 단계에 적용v-enter-to
: enter 상태의 마지막에 실행v-leave
: leave의 시작 상태v-leave-active
: leave 활성화 상태, 진입 단계에 적용v-leave-to
: leave 상태의 마지막에 실행가장 일반적인 트랜지션 유형 중 하나는 CSS 트랜지션이다.
<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#example-1',
data: {
show: true
}
})
/* 애니매이션 Enter∙leave의 지속 시간과 타이밍 기능을 다르게 사용할 수 있음 */
.slide-fade-enter-active {
transition: all .3s ease;
}
.slide-fade-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.slide-fade-enter, .slide-fade-leave-to
/* .slide-fade-leave-active below version 2.1.8 */ {
transform: translateX(10px);
opacity: 0;
}
CSS 애니메이션은 CSS 트랜지션과 같은 방식으로 적용된다.
<div id="example-2">
<button @click="show = !show">Toggle show</button>
<transition name="bounce">
<p v-if="show">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus.</p>
</transition>
</div>
new Vue({
el: '#example-2',
data: {
show: true
}
})
.bounce-enter-active {
animation: bounce-in .5s;
}
.bounce-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% { transform: scale(0); }
50% { transform: scale(1.5); }
100% { transform: scale(1); }
}
다음 속성을 제공하여 사용자 정의 트랜지션 클래스 지정(라이브러리 CSS)을 할 수 있다.
<link href="https://cdn.jsdelivr.net/npm/animate.css@3.5.1" rel="stylesheet" type="text/css">
<div id="example-3">
<button @click="show = !show">
Toggle render
</button>
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
<p v-if="show">hello</p>
</transition>
</div>
new Vue({
el: '#example-3',
data: {
show: true
}
})
Vue 트랜지션 종료에 대한 이벤트 리스너(transitionend, animationend) 가 제공된다.
엘리먼트에 CSS 트랜지션∙애니메이션 둘 중 하나만 사용하는 경우 Vue는 올바른 유형을 자동으로 감지할 수 있지만,
모두 적용한 경우 두 값을 모두 가지므로 하나에 대해 명시적으로 선언해야한다.
// ...
mounted() {
EventTarget.addEventListener('transitionend', () = > { ... })
// or
EventTarget.addEventListener('animationend', () = > { ... })
}
// ...
대부분의 경우 Vue는 트랜지션이 완료를 자동으로 감지할 수 있다.
기본적으로 Vue는 <transition>
내부 엘리먼트의 최상위의 transitionend
나 animationend
이벤트를 감지한다.
하지만 엘리먼트 내에 더 긴 트랜지션을 같는 자손이 있는 경우, duration 속성을 통해 명시적인 트랜지션 지속 시간(밀리초)을 설정할 수 있다.
<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
속성에 JavaScript 훅을 정의할 수 있다,
<transition
v-on:before-enter="beforeEnter"
v-on:enter="enter"
v-on:after-enter="afterEnter"
v-on:enter-cancelled="enterCancelled"
v-on:before-leave="beforeLeave"
v-on:leave="leave"
v-on:after-leave="afterLeave"
v-on:leave-cancelled="leaveCancelled"
>
<!-- ... -->
</transition>
// ...
methods: {
// ------------
// ENTERING 진입
// ------------
beforeEnter: function (el) {
// ...
},
// done 콜백은 CSS와 함께 사용할 때 선택 사항임
enter: function (el, done) {
// ...
done()
},
afterEnter: function (el) {
// ...
},
enterCancelled: function (el) {
// ...
},
// ------------
// LEAVING 진출
// ------------
beforeLeave: function (el) {
// ...
},
// done 콜백은 CSS와 함께 사용할 때 선택 사항임
leave: function (el, done) {
// ...
done()
},
afterLeave: function (el) {
// ...
},
// leaveCancelled은 v-show와 함께 사용
leaveCancelled: function (el) {
// ...
}
}
노드의 초기 렌더에 트랜지션을 적용하고 싶다면 appear 속성을 추가할 수 있다.
<transition appear>
<!-- ... -->
</transition>
<transition
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class" (2.1.8+)
appear-active-class="custom-appear-active-class"
>
<!-- ... -->
</transition>
<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>
v-if
∙v-else
를 사용하여 원본 엘리먼트 사이를 트랜지션 할 수 있다.
<transition>
<table v-if="items.length > 0">
<!-- ... -->
</table>
<p v-else>Sorry, no items found.</p>
</transition>
같은 태그명을 가진 엘리먼트끼리 트랜지션 할 경우, :key
속성을 부여하여 엘리먼트간 구분을 해줘야한다.
<transition>
<button v-if="isEditing" key="save">
Save
</button>
<button v-else key="edit">
Edit
</button>
</transition>
<!-- ↑ 같은 거 ↓ -->
<transition>
<button v-bind:key="isEditing">
{{ isEditing ? 'Save' : 'Edit' }}
</button>
</transition>
실제로 여러 개의 v-if
를 사용하거나 하나의 엘리먼트를 동적 속성에 바인딩 하여 여러 엘리먼트 사이를 트랜지션할 수 있다.
<transition>
<button v-if="docState === 'saved'" key="saved">
Edit
</button>
<button v-if="docState === 'edited'" key="edited">
Save
</button>
<button v-if="docState === 'editing'" key="editing">
Cancel
</button>
</transition>
↑ 같은 거 ↓
<transition>
<button v-bind:key="docState">
{{ buttonMessage }}
</button>
</transition>
// ...
computed: {
buttonMessage: function () {
switch (this.docState) {
case 'saved': return 'Edit'
case 'edited': return 'Save'
case 'editing': return 'Cancel'
}
}
}
엘리먼트 간 트랜지션은 동시에 발생한다.
때로는 복수의 엘리먼트들이 block 등으로 배치가 될 경우 위아래, 혹은 양옆에서 각각 동작하여 어색할 수 있기 때문에 Vue에선 시간차 트랜지션을 위한 mode 옵션을 제공한다.
<transition name="fade" mode="out-in">
<!-- ... -->
</transition>
컴포넌트 간 트랜지션은 더욱 간단하다.
key 속성 없이 <component :is />
를 통해 동적 컴포넌트를 래핑하기만 하면 된다.
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
new Vue({
el: '#transition-components-demo',
data: {
view: 'v-a'
},
components: {
'v-a': {
template: '<div>Component A</div>'
},
'v-b': {
template: '<div>Component B</div>'
}
}
})
.component-fade-enter-active, .component-fade-leave-active {
transition: opacity .3s ease;
}
.component-fade-enter, .component-fade-leave-to
/* .component-fade-leave-active below version 2.1.8 */ {
opacity: 0;
}
<transition-group>
: v-for
를 통해 반복되는 컴포넌트를 렌더링할 때, 각각의 모든 요소에 대해 트랜지션을 부여하고자 하는 경우 사용한다.
<transition>
과 달리, 실제 요소인 <span>
을 렌더링한다. tag 속성으로 렌더링 된 요소를 변경할 수 있음v-for
로 구현되므로 :key
속성이 필요<transition-group>
에 지정하지만, 트랜지션은 내부요소들에 적용된다.<div id="list-demo">
<button v-on:click="add">Add</button>
<button v-on:click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" v-bind:key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
new Vue({
el: '#list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9],
nextNum: 10
},
methods: {
randomIndex: function () {
return Math.floor(Math.random() * this.items.length)
},
add: function () {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove: function () {
this.items.splice(this.randomIndex(), 1)
},
}
})
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active, .list-leave-active {
transition: all 1s;
}
.list-enter, .list-leave-to /* .list-leave-active below version 2.1.8 */ {
opacity: 0;
transform: translateY(30px);
}
<transition-group>
은 enter∙leave 뿐만 아니라 엘리먼트들의 위치변화에 대해서도 트랜지션 적용이 가능하다.
위치가 바뀔 때 호출되는 v-move
클래스를 통해 트랜지션을 적용할 수 있다.
다른 클래스와 마찬가지로 각 클래스에는 트랜지션의 name 속성이 접두어로 붙으며,
move-class 속성을 사용하여 클래스를 수동으로 지정할 수 있다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
<div id="flip-list-demo" class="demo">
<button v-on:click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" v-bind:key="item">
{{ item }}
</li>
</transition-group>
</div>
new Vue({
el: '#flip-list-demo',
data: {
items: [1,2,3,4,5,6,7,8,9]
},
methods: {
shuffle: function () {
this.items = _.shuffle(this.items)
}
}
})
.flip-list-move {
transition: transform 1s;
}
Vue에서는 컴포넌트들의 위치가 변화할 때, 기본적으로 FLIP 기법을 통한 애니메이션으로 요소들을 부드럽게 트랜지션시켜준다.
트랜지션은 Vue의 컴포넌트 시스템을 통해 재사용 할 수 있다.
재사용할 수 있는 트랜지션을 만들려면 루트에 <transition>
또는 <transition-group>
컴포넌트를 놓은 후 자식을 트랜지션 컴포넌트에 전달하면 된다.
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
v-on:before-enter="beforeEnter"\
v-on:after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
})
Vue.component('my-special-transition', {
functional: true,
render: function (createElement, context) {
var data = {
props: {
name: 'very-special-transition',
mode: 'out-in'
},
on: {
beforeEnter: function (el) {
// ...
},
afterEnter: function (el) {
// ...
}
}
}
return createElement('transition', data, context.children)
}
})
Vue 트랜지션 시스템은 CSS와 라이프사이클 클래스 기반으로 Enter∙Leave, 리스트 애니메이션 등을 구현했다.
하지만, 데이터 자체에 대한 애니메이션에 대한 필요성도 존재할 것이다.
외부 애니메이션 함수를 사용해서 복잡하지만,
요지는 watch를 통해 해당 데이터에 대한 애니메이션 함수를 적용한다는 것이다.
상태 트랜지션에도 많은 섹션들이 있지만, 내용이 심오하고 활용성이 높지 않다고 생각되어 한 번 훑어 읽는 정도만 하는것을 추천한다.
watch
를 이용한 상태 애니메이션감시자를 사용하면 숫자 속성의 변경 사항을 다른 속성으로 애니메이션 할 수 있다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js"></script>
<div id="animated-number-demo">
<input v-model.number="number" type="number" step="20">
<p>{{ animatedNumber }}</p>
</div>
new Vue({
el: '#animated-number-demo',
data: {
number: 0,
tweenedNumber: 0
},
computed: {
animatedNumber: function() {
return this.tweenedNumber.toFixed(0);
}
},
watch: {
number: function(newValue) {
gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue });
}
}
})
<input>
의 v-model.number
가 바뀔때마다, watch를 통해 tweenedNumber 역시 갱신해주며 애니메이션을 적용한다.
여기에 연계된 computed의 animatedNumber가 컴포넌트에 표현되는데, 여기에 애니메이션이 적용되는 모습이다.
여러 상태 트랜지션을 관리하면 Vue 인스턴스 또는 컴포넌트의 복잡성이 빠르게 증가한다.
이때, 하위 컴포넌트로 만들어 각각 애니메이션이 실행되게 만들 수 있다.
<script src="https://cdn.jsdelivr.net/npm/tween.js@16.3.4"></script>
<div id="example-8">
<input v-model.number="firstNumber" type="number" step="20"> +
<input v-model.number="secondNumber" type="number" step="20"> =
{{ result }}
<p>
<animated-integer v-bind:value="firstNumber"></animated-integer> +
<animated-integer v-bind:value="secondNumber"></animated-integer> =
<animated-integer v-bind:value="result"></animated-integer>
</p>
</div>
Vue.component('animated-integer', {
template: '<span>{{ tweeningValue }}</span>',
props: {
value: {
type: Number,
required: true
}
},
data: function () {
return {
tweeningValue: 0
}
},
watch: {
value: function (newValue, oldValue) {
this.tween(oldValue, newValue)
}
},
mounted: function () {
this.tween(0, this.value)
},
methods: {
tween: function (startValue, endValue) {
var vm = this
function animate () {
if (TWEEN.update()) {
requestAnimationFrame(animate)
}
}
new TWEEN.Tween({ tweeningValue: startValue })
.to({ tweeningValue: endValue }, 500)
.onUpdate(function () {
vm.tweeningValue = this.tweeningValue.toFixed(0)
})
.start()
animate()
}
}
})
// 모든 Vue 인스턴스에서 모든 복잡성이 제거됨
new Vue({
el: '#example-8',
data: {
firstNumber: 20,
secondNumber: 40
},
computed: {
result: function () {
return this.firstNumber + this.secondNumber
}
}
})