티스토리 뷰

요즘 출시되는 웹 애플리케이션에서는 페이지 전환시 애니메이션 동작이 거의 필수로 들어가있다. 이러한 애니메이션은 꼭 네이티브가 아니라 vue로 만든 SPA에서 지정할 수 있다. vue에서는 내장 애니메이션 컴포넌트인 <transition>을 활용하면 된다.

transition을 사용하는 방법은 공식문서나 다른 분들이 올려놓은 블로그에 이미 잘 정리되어 있으므로 이번 글에서는 실질적으로 페이지 이동할 때마다 transition이 일어나게 하는 방법을 소개하겠다. 기존의 공식문서에서는 URL을 split('/')하여 몇 개의 부분으로 나뉘는 지를 기준으로 삼아서 toDepthfromDepth값을 비교하여 애니메이션 방식을 정하고 있다. 이 방식도 좋은 방법이기는 하지만, 라우팅 정책을 엄격하게 세워야하기 때문에 내가 하기에는 다소 불편하다고 판단하여 방법을 바꾸기로 했다.

일단 모든 페이지에 대해서 transition을 넣을 것이므로 단순하게 생각해보자면 <router-view /><trainsition> 감싸는 형태를 생각해볼 수 있다.

// App.vue
<transition name="transitionName">
  <router-view />
</transition>

다만 공식문서에서는 router에 transition을 넣는 경우, v-slot을 활용하여 넣는 방식을 안내하고 있으므로,

  // App.vue
  <router-view v-slot="{ Component }">
        <transition :name="transitionName">
            <component :is="Component" />
        </transition>
    </router-view>

이 방식이 더 적합한 방식이다. v-slot을 활용할 경우 <component>태그 안에 들어갈 컴포넌트를 동적으로 명시할 수 있으므로 이 방식을 소개한 것으로 보인다. 여기까지 했으면 이제 transitionName을 상황에 따라 지정해주면 된다. 이 글에서 소개한 transition은 특정 페이지로 이동 시 화면이 오른쪽에서 덮이는 slide-right와 뒤로 가기시 화면이 왼쪽에서 덮이는 slide-left 를 소개하려고 한다.

페이지 이동하는 경우를 감지하기 위해서는 vue-router의 beforeEach를 활용하는 경우도 있지만, App.vue 단에서도 watch를 사용하여 인자로 route.name을 넣으면 감지할 수 있다. 여기서 신경써야 하는 부분은 router를 통한 페이지 이동이든, 뒤로가기를 통한 이동이든 모두 watch에 감지되기 때문에 사용자의 행동이 둘 중에서 어떤 것인지 알 수가 없다는 점이다. 그래서 우리는 사용자가 뒤로가기를 눌렀을 때만 발동되는 트리거가 무엇인지 알아야 한다. 그 트리거는 바로 윈도우 이벤트 중 하나인 popstate이다. 따라서 이벤트가 발동되면 isGoBack이라는 변수를 true로 세팅해두고 watch 안에서는 이 값에 따라서 사용자가 뒤로가기를 눌렀는지, 아니면 router를 통한 이동이었는지를 판단할 수 있게 된다.

// App.vue - script
setup(){
  let transitionName = ref('none');
  let isGoBack = false;

  window.addEventlistener('popstate',()=>{
    isGoBack = true;
  })

  watch(()=>route.name,
    (to,from)=>{
      // 우선 효과 없는 상태로 초기화
      transitionName.value = 'none';
      if(isGoBack){
        // 뒤로가기
        transitionName.value = 'slide-left';
        isGoBack = false;
      } else {
        // 페이지 이동
        if(from === undefined) return; // 새로고침인 경우
        transitionName.value = 'slide-right';
      }
  })

  return{
    transitionName,
  }
}
// App.vue - style
/* 슬라이드 START */
.none-enter-active,
.none-leave-active {
    display: none;
}
.none-enter-from {
    display: none;
}
.none-leave-to {
    display: none;
}
/* slide right */
.slide-right-enter-active,
.slide-right-leave-active {
    transition: transform 0.2s ease;
}
.slide-right-enter-from {
    transform: translateX(100%);
}
.slide-right-leave-to {
    transform: translateX(-100%);
}
/* slide left */
.slide-left-enter-active,
.slide-left-leave-active {
    transition: transform 0.2s ease;
}
.slide-left-enter-from {
    transform: translateX(-100%);
}
.slide-left-leave-to {
    transform: translateX(100%);
}
/* 슬라이드 END */

댓글