Skip to content

5. 라우팅 애니메이션


Angular 애플리케이션에서는 라우터가 조건에 맞는 라우팅 규칙(route)을 선택하면서 화면을 전환한다. 그리고 Angular 라우터는 라우팅 규칙에 지정된 대로 URL 경로와 컴포넌트를 화면에 표시하는데, 이 때 화면이 전환되는 애니메이션을 적용하면 사용성을 크게 높일 수 있다.


Angular 라우터가 제공하는 애니메이션 기능은 라우팅 규칙이 변경될 때 다양하게 활용할 수 있다. 이 때 사용하는 애니메이션은 시퀀스로 지정하는데, 화면에서 호스트가 되는 최상위 컴포넌트부터 뷰 안에 포함된 컴포넌트에도 애니메이션을 지정할 수 있다.


화면 전환 애니메이션을 적용하려면 다음과 같이 작업한다:


1] 애플리케이션에 라우팅 모듈을 로드하고 라우팅 규칙을 등록한다.

2] 라우팅 규칙과 연결된 컴포넌트가 DOM에 표시되도록 라우팅 영역(router outlet)을 추가한다.

3] 애니메이션을 정의한다.


Home 화면에서 About 화면으로 이동하는 동안 HomeComponentAboutComponent에 애니메이션이 어떻게 적용되는지 살펴보자. 두 컴포넌트는 AppComponent의 자식 컴포넌트이다. 라우팅이 진행되는 동안 기존에 표시되던 화면은 오른쪽으로 이동하면서 사라지고, 새로운 화면은 왼쪽에서 들어오면서 표시되도록 예제 코드를 작성해 보자.


001


1. 라우팅 규칙 설정

먼저, RouterModule 클래스가 제공하는 메서드를 사용해서 라우팅 규칙을 정의한다. 라우터는 이 라우팅 규칙에 정의된 대로 화면을 전환한다.


이 예제에서는 RouterModule.forRoot() 함수를 사용해서 최상위 라우팅 규칙을 정의한다. 이 함수로 지정하는 라우팅 규칙은 AppModuleimports 배열에 추가해서 등록한다.


Note

  • RouterModule.forRoot() 메서드 실행 결과를 AppModule에 등록하면 앱 전역에 라우팅 규칙과 라우팅 관련 서비스 프로바이더가 등록된다.
  • 그래서 자식 모듈에서는 자연스럽게 서비스 프로바이더를 사용할 수 있으며, 라우팅 규칙을 지정하려면 RotuerModule.forChild() 메서드를 사용하면 된다.


이번 예제에서는 이렇게 정의한다.


src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { AppComponent } from './app.component';
import { OpenCloseComponent } from './open-close.component';
import { OpenClosePageComponent } from './open-close-page.component';
import { OpenCloseChildComponent } from './open-close.component.4';
import { ToggleAnimationsPageComponent } from './toggle-animations-page.component';
import { StatusSliderComponent } from './status-slider.component';
import { StatusSliderPageComponent } from './status-slider-page.component';
import { HeroListPageComponent } from './hero-list-page.component';
import { HeroListGroupPageComponent } from './hero-list-group-page.component';
import { HeroListGroupsComponent } from './hero-list-groups.component';
import { HeroListEnterLeavePageComponent } from './hero-list-enter-leave-page.component';
import { HeroListEnterLeaveComponent } from './hero-list-enter-leave.component';
import { HeroListAutoCalcPageComponent } from './hero-list-auto-page.component';
import { HeroListAutoComponent } from './hero-list-auto.component';
import { HomeComponent } from './home.component';
import { AboutComponent } from './about.component';
import { InsertRemoveComponent } from './insert-remove.component';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    RouterModule.forRoot([
      { path: '', pathMatch: 'full', redirectTo: '/enter-leave' },
      { path: 'open-close', component: OpenClosePageComponent },
      { path: 'status', component: StatusSliderPageComponent },
      { path: 'toggle', component: ToggleAnimationsPageComponent },
      { path: 'heroes', component: HeroListPageComponent,
        data: { animation: 'FilterPage' } },
      { path: 'hero-groups', component: HeroListGroupPageComponent },
      { path: 'enter-leave', component: HeroListEnterLeavePageComponent },
      { path: 'auto', component: HeroListAutoCalcPageComponent },
      { path: 'insert-remove', component: InsertRemoveComponent},
      { path: 'home', component: HomeComponent, data: { animation: 'HomePage' } },
      { path: 'about', component: AboutComponent, data: { animation: 'AboutPage' } },
    ])
  ],


코드에서 보면 homeabout 경로는 각각 HomeComponent, AboutComponent와 연결되어 있다. 그래서 URL이 변경되면 라우터가 HomeComponent, AboutComponent 인스턴스를 생성해서 화면에 표시한다.


그리고 라우팅 규칙에는 path, component 외에 data 프로퍼티를 사용할 수 있다. AppComponent가 등록된 라우팅 규칙에 data 프로퍼티를 추가하면 라우팅 규칙이 변경되는 시점에 이 데이터가 전달되기 때문에, 화면이 전환되는 동안 적용될 애니메이션이 이 데이터를 활용할 수 있다. 이 문서에서는 데이터 값을 활용해서 routeAnimation 트리거에 활용해 보자.


Note

  • data 객체에 사용한 프로퍼티 이름은 임의로 지정한 것이다.
  • 이 예제에서는 animation이라고 지정했으며, 다른 이름으로 사용해도 된다.


2. 라우팅 영역(router outlet)

라우팅 규칙을 선언하고 나면 라우팅 규칙에 연결된 컴포넌트가 화면에 표시될 위치를 지정해야 한다. 이번 예제에서는 AppComponent 템플릿에 <router-outlet>를 추가하는 방식으로 지정한다.


<router-outlet> 디렉티브는 현재 활성화된 라우팅 규칙에 대한 커스텀 데이터를 activatedRouteData 프로퍼티에 담고 있다. 라우팅 애니메이션에 이 데이터를 활용할 수 있다.


src/app/app.component.html
<div [@routeAnimations]="prepareRoute(outlet)">
  <router-outlet #outlet="outlet"></router-outlet>
</div>


AppComponent에는 화면이 전환되는 것을 감지할 수 있는 메서드를 정의한다. 이 메서드는 활성화된 라우팅 규칙에 있는 data 프로퍼티 값을 참고해서 애니메이션 트리거(@routeAnimations)에 적절한 상태를 연결한다. 아래와 같은 식으로 구현한다.


src/app/app.component.ts
prepareRoute(outlet: RouterOutlet) {
  return outlet?.activatedRouteData?.['animation'];
}


이 코드에서 preapreRoute() 메서드는 라우팅 영역 디렉티브(#outlet="outlet")를 인자로 받아서 현재 활성화된 라우팅 규칙과 이 규칙에 있는 데이터 값을 기반으로 적절한 상태를 문자열로 반환한다. 그래서 트랜지션은 이 데이터로 조절할 수 있다.


3. 애니메이션 정의하기

애니메이션은 컴포넌트에 직접 정의할 수 있다. 하지만 이번에는 이 애니메이션을 재사용할 수 있도록 별도 파일에 정의해 보자.


다음과 같은 애니메이션 파일을 작성한다.


src/app/animations.ts
export const slideInAnimation = trigger("routeAnimations", [
  transition("HomePage <=> AboutPage", [
    style({ position: "relative" }),
    query(":enter, :leave", [
      style({
        position: "absolute",
        top: 0,
        left: 0,
        width: "100%",
      }),
    ]),
    query(":enter", [style({ left: "-100%" })]),
    query(":leave", animateChild()),
    group([
      query(":leave", [animate("300ms ease-out", style({ left: "100%" }))]),
      query(":enter", [animate("300ms ease-out", style({ left: "0%" }))]),
    ]),
    query(":enter", animateChild()),
  ]),
  transition("* <=> FilterPage", [
    style({ position: "relative" }),
    query(":enter, :leave", [
      style({
        position: "absolute",
        top: 0,
        left: 0,
        width: "100%",
      }),
    ]),
    query(":enter", [style({ left: "-100%" })]),
    query(":leave", animateChild()),
    group([
      query(":leave", [animate("200ms ease-out", style({ left: "100%" }))]),
      query(":enter", [animate("300ms ease-out", style({ left: "0%" }))]),
    ]),
    query(":enter", animateChild()),
  ]),
]);


이 애니메이션은 다음과 같이 동작한다:


  • routeAnimations 트리거에 연결된 트랜지션은 두 개이다. 이 트랜지션은 동시에 시작된다.
  • 트랜지션이 진행되는 동안에는 호스트 엘리먼트와 자식 엘리먼트가 상대 위치로 조정된다.
  • 호스트 화면에 들어오는 자식 화면과 화면에서 나가는 자식 화면을 탐색하기 위해 query() 함수를 사용했다.


이제 라우팅 규칙이 변경되면 애니메이션 트리거가 발생하고 상태에 맞는 트랜지션이 시작된다.


Note

  • 트랜지션 상태는 라우팅 규칙에 정의한 data 프로퍼티와 맞아야 한다.


이렇게 정의한 애니메이션은 AppComponent 메타데이터의 animations 배열에 추가하면 애플리케이션에 등록할 수 있다.


src/app/app.component.ts
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [
    slideInAnimation
  ]
})


1) 호스트/자식 컴포넌트 스타일 지정하기

트랜지션이 진행되는 동안에는 새로운 화면이 이전에 있던 화면을 대체하는 애니메이션이 함께 진행되기 때문에 두 화면이 동시에 표시되는 순간이 있다. 이 문제를 해결하려면 호슽 화면이 상대적인 위치를 사용하도록 수정하고, 새로 들어오는 자식 화면은 절대(absolute) 위치를 사용하면 된다. 이렇게 수정하면 애니메이션이 적용된 하면이 제위치에서 움직이며 다른 화면에 영향을 주는 것도 방지할 수 있다.


src/app/animations.ts (일부)
trigger('routeAnimations', [
  transition('HomePage <=> AboutPage', [
    style({ position: 'relative' }),
    query(':enter, :leave', [
      style({
        position: 'absolute',
        top: 0,
        left: 0,
        width: '100%'
      })
    ]),


2) 뷰 컨테이너 쿼리하기

query() 메서드를 사용하면 현재 호스트 컴포넌트 안에 있는 엘리먼트를 탐색할 수 있다. 그래서 query(":enter")라는 실행문으로 화면에 추가되는 엘리먼트를 찾을 수 있으며 query(":leave") 실행문으로 화면에서 제거되는 엘리먼트를 찾을 수 있다.


HomeAbout으로 이동하는 경우를 생각해 보자.


src/app/animations.ts (일부)
query(':enter', [
    style({ left: '-100%' })
  ]),
  query(':leave', animateChild()),
  group([
    query(':leave', [
      animate('300ms ease-out', style({ left: '100%' }))
    ]),
    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ])
  ]),
  query(':enter', animateChild()),
]),
transition('* <=> FilterPage', [
  style({ position: 'relative' }),
  query(':enter, :leave', [
    style({
      position: 'absolute',
      top: 0,
      left: 0,
      width: '100%'
    })
  ]),
  query(':enter', [
    style({ left: '-100%' })
  ]),
  query(':leave', animateChild()),
  group([
    query(':leave', [
      animate('200ms ease-out', style({ left: '100%' }))
    ]),
    query(':enter', [
      animate('300ms ease-out', style({ left: '0%' }))
    ])
  ]),
  query(':enter', animateChild()),
])


이 때 애니메이션 코드는 다음과 같은 순서로 동작한다:


  • query(':enter style({ left: '-100%' })는 화면에 추가되는 엘리먼트에 매칭되며 처음에는 보이지 않도록 화면 왼쪽에 배치된다.
  • 화면에서 사라지는 엘리먼트는 animateChild()를 실행해서 자식 애니메이션을 시작한다.
  • 자식 애니메이션은 동시에 시작하기 위해 group() 함수를 사용했다.
  • group() 함수 안에서는:
  • 화면에서 사라지는 엘리먼트를 찾아서 화면 오른쪽으로 이동한다.
  • 새로운 화면을 일반 가속도 함수로 움직인다. 결국 about 화면은 화면 왼쪽부터 오른쪽으로 움직인다.
  • 새로운 화면이 나타나는 애니메이션이 끝난 후에 animateChild() 메서드가 실행되면서 자식 애니메이션이 시작된다.


지금까지 화면을 전환할 때 애니메이션을 적용하는 방법에 대해 알아봤다.


References