[Vue.js] Transition & Animation
근래의 웹 어플리케이션은 유저들의 조작에 따라 다양한 인터렉션 효과를 제공한다.
Vue는 여기에 사용되는 transition 효과를 좀 더 용이하게 사용할 수 있도록 <transition> 이라는 태그 컴포넌트 기능을 제공한다.
이번 포스팅에서는, Vue.js 공식문서의 "트랜지션 & 애니메이션" 내용을 기반으로 <transition> 태그 컴포넌트를 사용하는 방법에 대해 학습해보겠다.
💚 진입/진출 그리고 리스트 트랜지션
0. <transition> 컴포넌트
Vue 프론트화면 개발에는 많은 애니메이션이 적용된다. 이 때, 보편적으로 사용되는 방법이 CSS를 통한 제어일 것이다.
(transform 을 통한 변형, class를 동적으로 부여하여 제어한다. 이를, transition 설정을 통해 유하게 구현할 수 있다.)
Vue.js 에서는 CSS transition을 더 빠르고 간단하게 구현하는 <transition> 태그(컴포넌트)를 제공한다.
이를 사용하면, 하위(child) 엘리먼트에 트랜지션 효과를 부여할 뿐 아니라, 트랜지션 경과에 따른 상태관리도 가능하다.
<transition>
<div>Child Component</div>
</transition>
1. 단일 엘리먼트 / 컴포넌트 트랜지션
<div id="demo">
<button v-on:click="show = !show">
Toggle
</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
위 경우는 기본적인 트랜지션 사용법으로, <transition> 태그 내 요소가 조건부로 삽입/제거될 때 트랜지션이 동작한다.
- Vue는 대상 엘리먼트에 CSS 트랜지션 혹은 애니메이션 적용여부를 자동 감지한다. 존재한다면 적절하게 구동.
- <transition> 컴포넌트가 트랜지션 Javascript 훅을 제공하면, 이 훅이 각 시점별로 호출된다.
- CSS 트랜지션/애니메이션 혹은 Javascript 훅이 없는 경우 엘리먼트 삽입/제거 DOM 작업이 다음 프레임에 발생.
- 트랜지션 클래스
진입/진출 트랜지션에서는 아래와 같은 클래스들이 적용된다. 해당 클래스별로 트랜지션의 프레임을 활용할 수 있다.
"v" 에는 트랜지션 이름이 들어간다. (예를 들어, <transition name="fade"> 의 v-enter는 fade-enter)
- v-enter : Enter 시작상태. 엘리먼트가 삽입되기 전에 적용되고, 한 프레임 후에 제거.
- v-enter-active : Enter 활성 및 종료상태. 엘리먼트가 삽입되기 전에 적용되고, 트랜지션/애니메이션 완료 시 제거.
- v-enter-to : 엘리먼트가 삽입된 후(v-enter 삭제), 트랜지션이 끝나는 다음 프레임이 추가됨. (~ v2.1.8)
- v-leave : Leave 시작상태. 진출 트랜지션이 트리거될 때 적용되고, 한 프레임 후에 제거.
- v-leave-active : Leave 활성 및 종료상태. 진출 트랜지션이 트리거 시 적용되고, 트랜지션/애니메이션 완료 시 제거.
- v-leave-to : 진출 트랜지션이 트리거되고(v-leave 삭제), 트랜지션이 끝나는 다음 프레임이 추가됨. (~ v2.1.8)
- CSS 트랜지션 / 애니메이션
<div id="example-1">
<button @click="show = !show">
Toggle render
</button>
<transition name="slide-fade">
<p v-if="show">hello</p>
</transition>
</div>
.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를 통해 트랜지션을 적용한 예시이다.
slide-fade의 진입/진출의 active에 transition 설정을, 시작(enter) 및 종료(leave-to)에서 변경된 CSS속성을 부여한다.
.slide-fade-enter-active {
animation: bounce-in .5s;
}
.slide-fade-leave-active {
animation: bounce-in .5s reverse;
}
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
트랜지션 중간의 애니메이션도 마찬가지로 active에 설정해주면 된다.
- 사용자 지정 트랜지션 클래스
<transition> 컴포넌트에 직접 트랜지션 라이프 사이클에 대한 클래스를 부여할 수 있다. (기존기능 재활용에 용이)
<transition
name="custom-classes-transition"
enter-active-class="animated tada"
leave-active-class="animated bounceOutRight"
>
- 트랜지션과 애니메이션 함께 사용하기 & 명시적 트랜지션 지속 시간
Vue 트랜지션 종료에 대한 이벤트 리스너(transitionend, animationend) 가 제공된다.
엘리먼트에 CSS 트랜지션/애니메이션 모두 적용한 경우 두 값을 모두 가지므로, 하나에 대해 명시적으로 선언해야한다.
또한, <transition>은 내부 엘리먼트의 최상위의 종료를 감지한다.
하지만, 엘리먼트 내에 더 긴 트랜지션을 같는 자손이 있는 경우, 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>
Vue의 v-on 기능을 활용할 수 있는 각각의 트랜지션 라이프사이클에 대한 훅 역시 지원된다.
각 훅에 대한 함수들은 첫 번째 인자로 el(transition 엘리먼트)를 받으며, enter 및 leave는 두번째 인자로 done(종료 이후 콜백함수) 를 받는다.
2. appear (최초 렌더링 시 트랜지션)
노드의 초기 렌더에 트랜지션을 바로 적용하고자 한다면, appear 속성 추가를 통해 구현할 수 있다.
<transition appear>
<!-- ... -->
</transition>
마찬가지로, 사용자 정의 클래스, 사용자 정의 Javscript 훅 역시 지원한다.
<!-- 사용자 정의 클래스 -->
<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>
<!-- 사용자 정의 JS훅 -->
<transition
appear
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>
3. 엘리먼트 간 트랜지션
// if, else 2개 엘리먼트 트랜지션
<transition>
<button v-if="isEditing" key="save">
Save
</button>
<button v-else key="edit">
Edit
</button>
</transition>
// if 3개 엘리먼트 트랜지션
<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>
// or
<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'
}
}
}
트랜지션은 v-if, v-else와 같이 복수의 엘리먼트들에 대해서도 적용이 가능하다.
단, 같은 태그 name들을 가지는 엘리먼트끼리 트랜지션 할 경우, :key 속성을 부여하여 엘리먼트간 구분을 해줘야한다.
- mode (트랜지션 모드)
단, 엘리먼트 간 트랜지션은 동시에 발생한다.
그렇기에, 복수의 엘리먼트들이 block 등으로 배치가 될 경우 위아래, 혹은 양옆에서 각각 동작하여 어색할 수 있다.
이들을 { position: absolute; } 등으로 겹칠수도 있겠지만, Vue에선 시간차 트랜지션을 위한 mode 옵션도 제공한다.
- in-out : 처음에는 새로운 엘리먼트가 트랜지션되고, 완료되면 기존 엘리먼트가 트랜지션된다.
- out-in : 처음에는 기존 엘리먼트가 트랜지션되고, 완료되면 새로운 엘리먼트가 트랜지션된다.
<transition name="fade" mode="out-in">
<!-- ... the buttons ... -->
</transition>
4. 컴포넌트 간 트랜지션
컴포넌트 간 트랜지션은 더욱 간단하다. <component :is /> 를 통해 동적 컴포넌트를 래핑하기만 하면 된다.
<transition name="component-fade" mode="out-in">
<component v-bind:is="view"></component>
</transition>
.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;
}
5. 리스트 트랜지션
v-for를 통해 반복되는 컴포넌트를 렌더링할 때, 각각의 모든 요소에 대해 트랜지션을 부여하고자 하는 경우가 있다.
이 때는, <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>
.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>과 달리, 실제 요소인 <span>들을 렌더링한다. tag 속성을 통해 태그요소를 변경할 수 있다.
- mode 속성 적용이 불가하다. (조건부 렌더링을 하는 영역이 아니므로)
- 내부 엘리먼트들이 v-for로 구현되므로 :key 속성이 필요하다.
- name을 <transition-group>에 지정하지만, 트랜지션은 내부요소들에 적용된다. (외부 wrapper는 해당되지 않음)
- 리스트 이동 트랜지션
<transition-group>은 진입/진출 뿐만 아니라 엘리먼트들의 위치변화에 대해서도 트랜지션 적용이 가능하다.
위치가 바뀔 때 호출되는 v-move 클래스를 통해 트랜지션을 적용할 수 있다.
Vue에서는 컴포넌트들의 위치가 변화할 때, 기본적으로 FLIP 기법을 통한 애니메이션으로 요소들을 부드럽게 트랜지션시켜준다.
* FLIP(First-Last-Invert-Play) : 시작, 끝 위치를 설정한 뒤, 중간에 변경되는 위치와 상태를 계산하여 플레이하는 기법
* FLIP 트랜지션은 { display: inline; } 에 적용되지 않는다. 이외, block, inline-block, flex, grid 등등에 적용가능.
- 스태거링 목록 트랜지션
transition-group의 트랜지션 속성들을 JS 메서드로 구현했다. 인덱스 값에 따라 딜레이를 주면서 스태거링을 하는 예제.
6. 트랜지션 재사용
반복되는 트랜지션을 컴포넌트화하여 재활용하는 방법이다.
루트에 <transition> 혹은 <transition-group>을 설정하고, 내부요소를 <slot> 으로 받아오면 된다.
* <slot> : 특정 컴포넌트에 등록되는 하위 컴포넌트를 유동적으로 확장하기 위한 Vue.js 문법 (공식문서 링크)
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) {
// ...
}
}
})
7. 동적 트랜지션
Vue 트랜지션의 name을 v-bind로 전달하는 개념이다.
트랜지션 컴포넌트가 유사한 트랜지션들을 wrappring 하고, 이를 각 개소에서 동적으로 변형할 때 유용할 것 같다.
<transition v-bind:name="transitionName">
<!-- ... -->
</transition>
💚 상태(state) 트랜지션
Vue 트랜지션 시스템은 CSS와 라이프사이클 클래스 기반으로 진입/진출, 리스트 애니메이션 등을 구현했다.
하지만, 데이터 자체에 대한 애니메이션에 대한 필요성도 존재할 것이다.
- 숫자와 계산
- 색 표시
- SVG 노드 위치
- 엘리먼트의 크기 및 기타 속성
예제가 많고 외부 애니메이션 함수를 사용해서 복잡하지만, 요지는 watch를 통해 해당 데이터에 대한 애니메이션 함수를 적용한다는 것이다.
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.3/TweenMax.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) {
TweenLite.to(this.$data, 0.5, { tweenedNumber: newValue });
}
}
})
<input> 의 vm값인 number가 바뀔때마다, watch를 통해 tweenedNumber 역시 갱신해주며 애니메이션을 적용한다.
여기에 연계된 computed의 animatedNumber가 컴포넌트에 표현되는데, 여기에 애니메이션이 적용되는 모습이다.
* 상태 트랜지션에도 많은 섹션들이 있지만, 내용이 심오하고 활용성이 높지 않다고 생각되어 한 번 윤독정도만 하는것을 추천한다.
<transition> 기법을 공부하면서 실제 내 프로젝트에도 적용을 해보았다.
확실히, CSS 작성이 줄어드는 느낌이었고, 트랜지션 역시 클래스가 아닌 상태와 Props로 제어하는 특징이 있다.
이게 장점이자 단점이 될 수도 있는게, 우선 Boolean 상태를 통해 제어하기 때문에 코드는 매우 심플하다.
하지만, 이를 부모의 이벤트나 로직에 의해서 수정한다고 해보자.
class의 경우 단순히 JS의 queryselector + classList 문법으로 간단하게 토글링이 가능하지만,
data는 외부에서 수정하려면 이를 위한 Props나 메서드를 제작하거나, 논리가 맞다면 상태를 상위로 옮겨야한다.
물론, 이 경우 자식의 이벤트나 로직과 연동되는 부분도 생각할 것들이 많아지는 것을 이번에 경험했다.
어플리케이션을 제작하면서, 유사한 효과의 트랜지션을 재활용하기에는 이 <transition>이 정말 유용할 것 같다.
하지만, CSS + class 토글링과 둘 중 어느 방법이 더 마땅한지를 항상 고심한 뒤에 적용해야 불필요한 작업이 최소화될 것이라고 나의 짧은 식견을 공유드리며 이 글을 마친다!
- Vue 공식문서 : https://kr.vuejs.org/v2/guide/transitions
- 캡틴판교 님의 블로그(Slots 관련) : https://joshua1988.github.io/web-development/vuejs/slots/
- jong-hui 님의 블로그(FLIP 기법 관련) : https://jong-hui.github.io/devlog/2019/10/30/flip/