Skip to content

3. 복잡한 시퀀스 처리하기


지금까지는 HTML 엘리먼트 하나에 애니메이션을 하나만 연결해 봤다. 그런데 Angular에서는 복잡한 순서로 진행되는 애니메이션도 구현할 수 있다. 그리드 전체가 움직이거나 목록에 있는 엘리먼트 각각이 화면에 나타나거나 화면에서 사라지는 애니메이션도 구현할 수 있다. 이런 애니메이션은 동시에 실행할 수도 있으며 순서대로 실행할 수도 있고, 다른 애니메이션이 끝나면 실행할 수 있다.


복잡한 애니메이션 시퀀스는 다음 함수들을 사용해서 구현한다:


  • query(): 자식 HTML 엘리먼트를 찾을 떄 사용한다.
  • stagger(): 엘리먼트 여러 개에 있는 애니메이션에 순차적으로 딜레이를 줄 때 사용한다.
  • group(): 여러 애니메이션을 동시에 시작할 때 사용한다.
  • sequence(): 애니메이션을 순서대로 시작할 때 사용한다.


1. 여러 엘리먼트에 있는 애니메이션 시작하기: query(), stagger()

query() 함수를 사용하면 자식 엘리먼트 중에서 애니메이션이 필요한 엘리먼트를 탐색할 수 있다. 이 함수는 부모 컴포넌트를 기준으로 HTML 엘리먼트를 찾아서 개별 엘리먼트에 애니메이션을 적용할 수 있다. 개발자가 지정하지 않은 세세한 설정은 Angular가 알아서 처리한다.


stagger() 함수를 사용하면 이렇게 쿼리한 항목을 순차적으로 시작할 수 있도록 지연시간을 조정할 수 있다.


라이브 예제 앱에서 Filter/Stagger 탭을 보면 히어로의 목록이 순서대로 표시된다. 이 때 애니메이션이 적용된 항목은 히어로 목록 전체이며, 위에서 아래로 순차적으로 표시되도록 구현되었다.


아래 예제를 보면 query() 함수와 stagger() 함수가 사용된 것을 확인할 수 있다.


  • 화면에 나타나는 엘리먼트는 query()로 탐색한다.
  • 애니메이션이 적용될 엘리먼트의 초기 스타일을 지정하기 위해 style()을 사용했다. 이번 예제에서는 transform을 사용해서 화면 밖에 있다가 나타나도록 구현했다.
  • 각 애니메이션을 30ms마다 순서대로 실행하기 위해 stagger() 함수를 사용했다.
  • 각 애니메이션은 0.5초에 걸쳐 진행되며, 가속도 커브는 커스텀으로 지정했고 투명도가 조절되고 transform이 해제되는 방식으로 화면에 표시된다.


src/app/hero-list-page.component.ts
animations: [
  trigger('pageAnimations', [
    transition(':enter', [
      query('.hero', [
        style({opacity: 0, transform: 'translateY(-100px)'}),
        stagger(30, [
          animate('500ms cubic-bezier(0.35, 0, 0.25, 1)',
          style({ opacity: 1, transform: 'none' }))
        ])
      ])
    ])
  ]),


2. 애니메이션 동시에 시작하기: group()

개별 애니메이션은 조금씩 딜레이를 주면서 시작할 수도 있지만 동시에 시작하는 애니메이션이 필요한 때도 있다. 한 엘리먼트에 CSS 프로퍼티 2개를 애니메이션으로 조정하지만 이 애니메이션에 서로 다른 easing 함수를 사용하는 경우가 그렇다. 이렇게 구현하려면 group() 함수를 사용하면 된다.


Note

  • group() 함수는 여러 엘리먼트를 묶는 것이 아니라 애니메이션 스텝(step)을 묶는 용도로 사용한다.


아래 코드는 :enter 트랜지션과 :leave 트랜지션에 서로 다른 타이밍을 지정하는 예제 코드이다. 한 엘리먼트에 있는 애니메이션을 동시에 시작하더라도 이 애니메이션은 서로 독립적으로 동작한다.


src/app/hero-list-groups.component.ts (일부)
animations: [
  trigger("flyInOut", [
    state(
      "in",
      style({
        width: 120,
        transform: "translateX(0)",
        opacity: 1,
      })
    ),
    transition(":enter", [
      style({ width: 10, transform: "translateX(50px)", opacity: 0 }),
      group([
        animate(
          "0.3s 0.1s ease",
          style({
            transform: "translateX(0)",
            width: 120,
          })
        ),
        animate(
          "0.3s ease",
          style({
            opacity: 1,
          })
        ),
      ]),
    ]),
    transition(":leave", [
      group([
        animate(
          "0.3s ease",
          style({
            transform: "translateX(50px)",
            width: 10,
          })
        ),
        animate(
          "0.3s 0.2s ease",
          style({
            opacity: 0,
          })
        ),
      ]),
    ]),
  ]),
];


3. 순서대로 시작하기 vs. 동시에 시작하기

복잡한 애니메이션은 한 번에 모든 것을 처리할 수도 있다. 하지만 어떤 애니메이션이 끝난 이후에 다른 애니메이션을 시작해야 한다면 어떻게 해야 할까?


이전 섹션에서는 group() 함수를 사용해서 여러 애니메이션을 동시에 시작하는 방법에 대해 알아봤다. 이번에는 sequence() 함수를 사용해서 애니메이션이 끝난 후에 다른 애니메이션이 시작되도록 구현해 보자. sequence() 함수에서 각 애니메이션 단계는 style()이나 animate() 함수로 구성된다.


  • 스타일을 바로 반영하려면 style()을 사용한다.
  • 스타일을 천천히 전환하려면 animate()를 사용한다.


4. 필터 애니메이션 예제

예제 앱에 있는 다른 애니메이션에 대해 알아보자. Filter/Stagger 탭에 있는 Search Heroes 입력 필드에 Magnet이나 tornado와 같은 텍스트를 입력해 보자.


그러면 사용자가 글자를 하나씩 입력할 때마다 검색조건에 해당되지 않는 엘리먼트는 화면에서 사라지는 필터가 동작한다. 그리고 글자를 하나씩 지우면 변경된 조건에 맞는 엘리먼트는 다시 화면에 나타난다.


이 애니메이션이 적용된 컴포넌트 템플릿은 이렇다. 트리거 이름은 filterAnimation이다.


src/app/hero-list-page.component.html
<label for="search">Search heroes: </label>
<input
  type="text"
  id="search"
  #criteria
  (input)="updateCriteria(criteria.value)"
  placeholder="Search heroes"
/>

<ul class="heroes" [@filterAnimation]="heroesTotal"></ul>


그리고 컴포넌트 클래스 파일의 내용은 이렇다.


src/app/hero-list-page.component.ts
@Component({
  animations: [
    trigger("filterAnimation", [
      transition(":enter, * => 0, * => -1", []),
      transition(":increment", [
        query(
          ":enter",
          [
            style({ opacity: 0, width: 0 }),
            stagger(50, [
              animate("300ms ease-out", style({ opacity: 1, width: "*" })),
            ]),
          ],
          { optional: true }
        ),
      ]),
      transition(":decrement", [
        query(":leave", [
          stagger(50, [
            animate("300ms ease-out", style({ opacity: 0, width: 0 })),
          ]),
        ]),
      ]),
    ]),
  ],
})
export class HeroListPageComponent implements OnInit {
  heroesTotal = -1;

  get heroes() {
    return this._heroes;
  }
  private _heroes: Hero[] = [];

  ngOnInit() {
    this._heroes = HEROES;
  }

  updateCriteria(criteria: string) {
    criteria = criteria ? criteria.trim() : "";

    this._heroes = HEROES.filter((hero) =>
      hero.name.toLowerCase().includes(criteria.toLowerCase())
    );
    const newTotal = this.heroes.length;

    if (this.heroesTotal !== newTotal) {
      this.heroesTotal = newTotal;
    } else if (!criteria) {
      this.heroesTotal = -1;
    }
  }
}


애니메이션은 이렇게 동작한다:


  • 사용자가 화면을 전환할 때 진행되는 애니메이션은 무시한다. 필터가 처음 동작할 때는 해당 화면의 HTML 엘리먼트는 모두 DOM에 존재하고 있던 것으로 간주한다.
  • 키를 입력할 때마다 필터가 동작한다.


각 키 입력마다:


  • 사라지는 엘리먼트는 opacitywidth를 조절한다.
  • 엘리먼트 애니메이션은 300ms동안 진행된다.
  • 조건에 맞는 엘리먼트가 여러 개라면 각 엘리먼트마다 50ms 딜레이를 두면서 순차적으로 시작한다.


5. 애니메이션 시퀀스 정리

query() 함수를 사용하면 자식 엘리먼트 중에서 애니메이션을 적용할 엘리먼트를 탐색할 수 있다. 그래서 <div> 안에 있는 모든 이미지 엘리먼트를 모으는 용도로 활용할 수 있다. 그리고 stagger()group(), sequence()를 사용하면 여러 애니메이션이 어떻게 시작될지 지정할 수 있다.


References