Vue Transitions

YEZI🎐·2022년 12월 5일
0

Vue

목록 보기
24/45

Vue는 항목(html tag, component)이 DOM에 삽입, 갱신 또는 제거 될 때 트랜지션 효과를 적용하는 다양한 방법을 제공한다.

  • CSS 트랜지션 및 애니메이션을 위한 클래스를 자동으로 적용
  • Animate.css와 같은 타사 CSS 애니메이션 라이브러리 통합
  • 트랜지션 훅 중 JavaScript를 사용하여 DOM을 직접 조작
  • Velocity.js와 같은 써드파티 JavaScript 애니메이션 라이브러리 통합

싱글 엘리먼트∙컴포넌트 트랜지션

Vue는 transition wrapper component<transition> 을 제공하여 다음과 같은 상황에서 모든 엘리먼트 또는 컴포넌트에 대한 Enter∙Leave 트랜지션을 추가 할 수 있다.

  • 조건부 rendering (v-if 사용)
  • 조건부 display (v-show 사용)
  • 동적 컴포넌트
  • 컴포넌트 루트 노드
<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;
}
  • Vue는 대상 엘리먼트에 CSS 트랜지션 또는 애니메이션이 적용되었는지 여부를 자동으로 감지하여 CSS 트랜지션 클래스가 적절한 타이밍에 추가∙제거된다.
  • 트랜지션 컴포넌트가 JavaScript 훅을 제공하면 이러한 훅은 적절한 타이밍에 호출된다.
  • CSS 트랜지션∙애니메이션이 감지되지 않고 JavaScript 훅이 제공되지 않으면 삽입 또는 제거를 위한 DOM 작업이 다음 프레임에서 즉시 실행된다.
    (Vue의 nextTick 개념과는 다른 브라우저 애니메이션 프레임이다.)

트랜지션 클래스

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 트랜지션

가장 일반적인 트랜지션 유형 중 하나는 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 애니메이션은 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); }
}

Custom 트랜지션 클래스

다음 속성을 제공하여 사용자 정의 트랜지션 클래스 지정(라이브러리 CSS)을 할 수 있다.

  • enter-class
  • enter-active-class
  • enter-to-class (2.1.8+)
  • leave-class
  • leave-active-class
  • leave-to-class (2.1.8+)
<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> 내부 엘리먼트의 최상위의 transitionendanimationend 이벤트를 감지한다.
하지만 엘리먼트 내에 더 긴 트랜지션을 같는 자손이 있는 경우, duration 속성을 통해 명시적인 트랜지션 지속 시간(밀리초)을 설정할 수 있다.

<transition :duration="1000">...</transition>
<transition :duration="{ enter: 500, leave: 800 }">...</transition>

Javascript 훅

속성에 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-ifv-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 옵션을 제공한다.

  • in-out : 처음에는 새로운 엘리먼트가 트랜지션되고, 완료되면 기존 엘리먼트가 트랜지션된다.
  • out-in : 처음에는 기존 엘리먼트가 트랜지션되고, 완료되면 새로운 엘리먼트가 트랜지션된다.
<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 속성으로 렌더링 된 요소를 변경할 수 있음
  • 조건부 렌더링을 하는 영역이 아니므로 transition mode 속성 사용할 수 없음
  • 내부 엘리먼트들이 v-for로 구현되므로 :key 속성이 필요
  • name을 <transition-group>에 지정하지만, 트랜지션은 내부요소들에 적용된다.
    (외부 wrapper는 해당되지 않음)
<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 기법을 통한 애니메이션으로 요소들을 부드럽게 트랜지션시켜준다.

  • FLIP(First-Last-Invert-Play) : 시작, 끝 위치를 설정한 뒤, 중간에 변경되는 위치와 상태를 계산하여 플레이하는 기법
  • FLIP 트랜지션은 { display: inline; } 에 적용되지 않음
    (이 외 block, inline-block, flex, grid 등등에 적용가능)

트랜지션 재사용

트랜지션은 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, 리스트 애니메이션 등을 구현했다.
하지만, 데이터 자체에 대한 애니메이션에 대한 필요성도 존재할 것이다.

  • 숫자와 계산
  • 색 표시
  • SVG 노드 위치
  • 엘리먼트의 크기 및 기타 속성

외부 애니메이션 함수를 사용해서 복잡하지만,
요지는 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
    }
  }
})


References

profile
까먹지마도토도토잠보🐘

0개의 댓글