ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Vue.js] Transition & Animation
    Front-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> 태그 내 요소가 조건부로 삽입/제거될 때 트랜지션이 동작한다.

    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/

    반응형
Designed by Tistory.