Front-End(Web)/Vue

[Vue.js] Transition & Animation

ttaeng_99 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> 태그 내 요소가 조건부로 삽입/제거될 때 트랜지션이 동작한다.

  1. Vue는 대상 엘리먼트에 CSS 트랜지션 혹은 애니메이션 적용여부를 자동 감지한다. 존재한다면 적절하게 구동.
  2. <transition> 컴포넌트가 트랜지션 Javascript 훅을 제공하면, 이 훅이 각 시점별로 호출된다.
  3. 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/

반응형