-
[Vue.js] Transition & AnimationFront-End(Web)/Vue 2021. 7. 14. 02:24반응형
근래의 웹 어플리케이션은 유저들의 조작에 따라 다양한 인터렉션 효과를 제공한다.
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/
반응형'Front-End(Web) > Vue' 카테고리의 다른 글
[Vue.js] 재사용 & 컴포지션 - (2) 플러그인 & 필터 (0) 2021.08.04 [Vue.js] 재사용 & 컴포지션 - (1) Mixin, Directive, Render (0) 2021.07.20 [Vue.js] 컴포넌트 톺아보기 (0) 2021.06.14 [Vue.js] Vuex - (3) Store 모듈화 (0) 2021.06.13 [Vue.js] Vuex - (2) Helper 함수 (0) 2021.06.11