Angular 생태계
애니메이션(이전 방식)

애니메이션 전환과 트리거

중요: @angular/animations 패키지는 이제 사용 중단 권고(deprecated) 상태입니다. Angular 팀은 새로 작성하는 모든 코드에서 animate.enter와 animate.leave를 사용하는 네이티브 CSS 애니메이션을 권장합니다. 새로운 진입 및 퇴장 애니메이션 가이드에서 자세히 알아보세요. Angular 애니메이션 패키지에서 마이그레이션하기를 참고하면 애플리케이션을 순수 CSS 애니메이션으로 전환하는 방법도 확인할 수 있습니다.

이 가이드에서는 * 와일드카드와 void 같은 특별한 전환 상태를 자세히 살펴봅니다. 이런 상태를 뷰에 진입하거나 뷰에서 퇴장하는 엘리먼트에 사용하는 방법도 설명합니다. 여러 애니메이션 트리거, 애니메이션 콜백, 키프레임으로 시퀀스 기반 애니메이션을 만드는 방법도 알아봅니다.

사전 정의된 상태와 와일드카드 매칭

Angular에서는 state() 함수로 전환 상태를 명시적으로 정의하거나, 미리 정의된 * 와일드카드와 void 상태를 사용할 수 있습니다.

와일드카드 상태

별표 *, 즉 와일드카드 는 모든 애니메이션 상태와 일치합니다. HTML 엘리먼트의 시작 상태나 종료 상태에 관계없이 적용되는 전환을 정의할 때 유용합니다.

예를 들어 open => * 전환은 엘리먼트의 상태가 open에서 다른 어떤 상태로든 바뀔 때 적용됩니다.

와일드카드 상태 표현식

다음 코드는 앞에서 살펴본 open, closed 상태 예제에 와일드카드 상태를 함께 사용한 예제입니다. 각 상태 사이의 전환 쌍을 일일이 정의하는 대신, closed로 바뀌는 모든 전환은 1초, open으로 바뀌는 모든 전환은 0.5초가 걸리도록 정의합니다.

이렇게 하면 새 상태마다 별도의 전환을 추가하지 않아도 됩니다.

open-close.ts

animations: [
  trigger('openClose', [
    // ...
    state(
      'open',
      style({
        height: '200px',
        opacity: 1,
        backgroundColor: 'yellow',
      }),
    ),
    state(
      'closed',
      style({
        height: '100px',
        opacity: 0.8,
        backgroundColor: 'blue',
      }),
    ),
    transition('* => closed', [animate('1s')]),
    transition('* => open', [animate('0.5s')]),
  ]),
],

양방향 화살표 문법을 사용하면 두 상태 사이의 양방향 전환을 지정할 수 있습니다.

open-close.ts

transition('open <=> closed', [animate('0.5s')]),

여러 전환 상태에 와일드카드 상태 사용하기

상태가 두 개인 버튼 예제에서는 가능한 상태가 open과 closed뿐이므로 와일드카드가 크게 유용하지 않습니다. 일반적으로 엘리먼트가 여러 상태로 바뀔 수 있을 때 와일드카드 상태를 사용합니다. 버튼이 open에서 closed뿐 아니라 inProgress 같은 상태로도 바뀔 수 있다면, 와일드카드 상태로 작성해야 할 코드의 양을 줄일 수 있습니다.

상태 세 개에 사용하는 와일드카드 상태

open-close.ts

animations: [
  trigger('openClose', [
    // ...
    state(
      'open',
      style({
        height: '200px',
        opacity: 1,
        backgroundColor: 'yellow',
      }),
    ),
    state(
      'closed',
      style({
        height: '100px',
        opacity: 0.8,
        backgroundColor: 'blue',
      }),
    ),
    transition('open => closed', [animate('1s')]),
    transition('closed => open', [animate('0.5s')]),
    transition('* => closed', [animate('1s')]),
    transition('* => open', [animate('0.5s')]),
    transition('open <=> closed', [animate('0.5s')]),
    transition('* => open', [animate('1s', style({opacity: '*'}))]),
    transition('* => *', [animate('1s')]),

* => * 전환은 두 상태 사이에 어떤 변화가 발생하든 적용됩니다.

전환은 정의한 순서대로 매칭됩니다. 따라서 * => * 전환보다 다른 전환을 우선해서 적용할 수 있습니다. 예를 들어 open => closed에만 적용할 스타일 변경이나 애니메이션을 정의하고, 따로 지정하지 않은 상태 쌍에는 * => *를 대체 전환으로 사용하세요.

이렇게 하려면 더 구체적인 전환을 * => * 앞에 나열합니다.

스타일에 와일드카드 사용하기

스타일 값에 와일드카드 *를 사용하면 현재 스타일 값이 무엇이든 그 값을 사용해서 애니메이션을 적용합니다. 와일드카드는 애니메이션을 적용할 상태가 트리거 안에 선언되지 않았을 때 사용하는 대체 값입니다.

open-close.ts

transition('* => open', [animate('1s', style({opacity: '*'}))]),

void 상태

페이지에 진입하거나 페이지에서 퇴장하는 엘리먼트의 전환을 설정하려면 void 상태를 사용하세요. 뷰 진입과 퇴장에 애니메이션 적용하기를 참고하세요.

와일드카드와 void 상태 조합하기

전환에서 와일드카드와 void 상태를 조합하면 페이지에 진입하거나 페이지에서 퇴장하는 애니메이션을 실행할 수 있습니다:

  • * => void 전환은 엘리먼트가 퇴장하기 전의 상태와 관계없이 뷰에서 퇴장할 때 적용됩니다.
  • void => * 전환은 엘리먼트가 진입하면서 갖게 되는 상태와 관계없이 뷰에 진입할 때 적용됩니다.
  • 와일드카드 상태 *는 void를 포함한 모든 상태와 일치합니다.

뷰 진입과 퇴장에 애니메이션 적용하기

이 절에서는 페이지에 진입하거나 페이지에서 퇴장하는 엘리먼트에 애니메이션을 적용하는 방법을 살펴봅니다.

다음 동작을 추가해 봅시다:

  • 영웅 목록에 영웅을 추가하면 페이지 왼쪽에서 날아 들어옵니다.
  • 목록에서 영웅을 제거하면 오른쪽으로 날아 나갑니다.

hero-list-enter-leave.ts

animations: [
  trigger('flyInOut', [
    state('in', style({transform: 'translateX(0)'})),
    transition('void => *', [style({transform: 'translateX(-100%)'}), animate(100)]),
    transition('* => void', [animate(100, style({transform: 'translateX(100%)'}))]),
  ]),
],

위 코드에서는 HTML 엘리먼트가 뷰에 연결되어 있지 않을 때 void 상태를 적용했습니다.

:enter와 :leave 별칭

:enter와 :leave는 각각 void => *와 * => void 전환의 별칭입니다. 여러 애니메이션 함수에서 이 별칭을 사용합니다.

transition ( ':enter', [ … ] ); // void => _의 별칭
transition ( ':leave', [ … ] ); // _ => void의 별칭

뷰에 진입하는 엘리먼트는 아직 DOM에 없으므로 선택하기가 더 어렵습니다. 뷰에 삽입되거나 뷰에서 제거되는 HTML 엘리먼트를 선택하려면 :enter와 :leave 별칭을 사용하세요.

*ngIf, *ngFor와 함께 :enter, :leave 사용하기

:enter 전환은 *ngIf나 *ngFor 뷰가 페이지에 배치될 때 실행되고, :leave는 이 뷰가 페이지에서 제거될 때 실행됩니다.

중요: 진입 및 퇴장 동작은 때로 혼란스러울 수 있습니다. 대체로 Angular가 DOM에 추가하는 모든 엘리먼트는 :enter 전환을 거친다고 생각하면 됩니다. :leave 전환은 Angular가 DOM에서 직접 제거하는 엘리먼트만 거칩니다. 예를 들어 부모가 DOM에서 제거되면서 함께 제거되는 엘리먼트의 뷰는 직접 제거되는 경우와 다릅니다.

이 예제에는 진입 및 퇴장 애니메이션을 위한 myInsertRemoveTrigger라는 특별한 트리거가 있습니다. HTML 템플릿에는 다음 코드가 들어갑니다.

insert-remove.html

@if (isShown) {
  <div @myInsertRemoveTrigger class="insert-remove-container">
    <p>The box is inserted</p>
  </div>
}

컴포넌트 파일의 :enter 전환은 초기 불투명도를 0으로 설정합니다. 그런 다음 엘리먼트가 뷰에 삽입될 때 불투명도가 1로 바뀌도록 애니메이션을 적용합니다.

insert-remove.ts

trigger('myInsertRemoveTrigger', [
  transition(':enter', [style({opacity: 0}), animate('100ms', style({opacity: 1}))]),
  transition(':leave', [animate('100ms', style({opacity: 0}))]),
]),

이 예제에서는 state()를 사용할 필요가 없습니다.

:increment와 :decrement 전환

transition() 함수에는 :increment와 :decrement라는 다른 셀렉터 값도 전달할 수 있습니다. 숫자 값이 증가하거나 감소할 때 전환을 실행하려면 이 셀렉터를 사용하세요.

도움말: 다음 예제에서는 query()와 stagger() 메서드를 사용합니다. 자세한 내용은 복잡한 시퀀스 문서를 참고하세요.

hero-list-page.ts

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}))])]),
  ]),
]),

전환에서 불리언 값 사용하기

트리거에 불리언 값이 바인딩되어 있다면, true와 false 또는 1과 0을 비교하는 transition() 표현식으로 매칭할 수 있습니다.

open-close.html

<div [@openClose]="isOpen ? true : false" class="open-close-container">
</div>

위 코드의 HTML 템플릿은 <div> 엘리먼트를 openClose 트리거에 바인딩하며, 상태 표현식으로 true 또는 false가 될 수 있는 isOpen을 사용합니다. 이 패턴은 open과 close처럼 이름을 붙인 상태 두 개를 만드는 방법 대신 사용할 수 있습니다.

@Component 메타데이터의 animations: 프로퍼티 안에서 상태가 true로 평가되면, 연결된 HTML 엘리먼트의 높이는 와일드카드 스타일 값 또는 기본값이 됩니다. 이 경우 애니메이션은 시작하기 전에 엘리먼트가 가지고 있던 높이를 사용합니다. 엘리먼트가 closed 상태가 되면 높이가 0으로 바뀌는 애니메이션이 적용되어 보이지 않게 됩니다.

open-close.ts

animations: [
  trigger('openClose', [
    state('true', style({height: '*'})),
    state('false', style({height: '0px'})),
    transition('false <=> true', animate(500)),
  ]),
],

여러 애니메이션 트리거

컴포넌트에 애니메이션 트리거를 여러 개 정의할 수 있습니다. 서로 다른 엘리먼트에 애니메이션 트리거를 연결하면, 엘리먼트 사이의 부모·자식 관계가 애니메이션의 실행 방식과 시점에 영향을 줍니다.

부모·자식 애니메이션

Angular에서 애니메이션이 실행될 때는 항상 부모 애니메이션이 우선하고 자식 애니메이션은 차단됩니다. 자식 애니메이션을 실행하려면 부모 애니메이션에서 자식 애니메이션이 있는 각 엘리먼트를 조회해야 합니다. 그런 다음 animateChild() 함수로 자식 애니메이션을 실행할 수 있습니다.

HTML 엘리먼트의 애니메이션 비활성화하기

HTML 엘리먼트에 @.disabled라는 특별한 애니메이션 제어 바인딩을 지정하면 해당 엘리먼트와 그 안에 중첩된 엘리먼트의 애니메이션을 끌 수 있습니다. @.disabled 바인딩이 true이면 모든 애니메이션이 렌더링되지 않습니다.

다음 예제에서 이 기능의 사용법을 확인할 수 있습니다.

@.disabled 바인딩이 true이면 @childAnimation 트리거는 실행되지 않습니다.

HTML 템플릿의 엘리먼트에서 @.disabled 호스트 바인딩으로 애니메이션을 끄면, 모든 내부 엘리먼트의 애니메이션도 꺼집니다. 하나의 엘리먼트에 있는 여러 애니메이션을 선택적으로 끌 수는 없습니다.

부모의 애니메이션이 비활성화되어 있어도 다음 방법으로 일부 자식 애니메이션을 실행할 수 있습니다:

  • 부모 애니메이션에서 query() 함수로 HTML 템플릿의 비활성화된 영역에 있는 내부 엘리먼트를 조회할 수 있습니다. 이 엘리먼트에는 여전히 애니메이션을 적용할 수 있습니다.
  • 부모가 자식 애니메이션을 조회한 뒤 animateChild() 함수로 실행할 수 있습니다.

모든 애니메이션 비활성화하기

Angular 애플리케이션의 모든 애니메이션을 끄려면 최상위 Angular 컴포넌트에 @.disabled 호스트 바인딩을 지정하세요.

app.ts

@Component({
  selector: 'app-root',
  templateUrl: 'app.html',
  styleUrls: ['app.css'],
  imports: [RouterLink, RouterOutlet],
  animations: [
    slideInAnimation,
  ],
})
export class AppComponent {
  @HostBinding('@.disabled')
  public animationsDisabled = false;
}

도움말: 애플리케이션 전체의 애니메이션을 비활성화하는 기능은 엔드 투 엔드(E2E) 테스트에서 유용합니다.

애니메이션 콜백

애니메이션 trigger() 함수는 애니메이션이 시작될 때와 끝날 때 콜백 을 발생시킵니다. 다음 예제의 컴포넌트에는 openClose 트리거가 있습니다.

open-close.ts

@Component({
  selector: 'app-open-close',
  animations: [
    trigger('openClose', [
      // ...
    ]),
  ],
  templateUrl: 'open-close.html',
  styleUrls: ['open-close.css'],
})
export class OpenClose {
  onAnimationEvent(event: AnimationEvent) {
  }
}

HTML 템플릿에서는 @triggerName.start와 @triggerName.done으로 애니메이션 이벤트를 수신하고, $event로 이벤트를 전달받습니다. 여기서 triggerName은 사용하는 트리거 이름입니다. 이 예제의 openClose 트리거는 다음과 같이 사용합니다.

open-close.html

<div
  [@openClose]="isOpen ? 'open' : 'closed'"
  (@openClose.start)="onAnimationEvent($event)"
  (@openClose.done)="onAnimationEvent($event)"
  class="open-close-container"
>
</div>

애니메이션 콜백은 데이터베이스 조회처럼 느린 API 호출의 대기 시간을 보완하는 용도로 사용할 수 있습니다. 예를 들어 백엔드 시스템의 작업이 끝날 때까지 InProgress 버튼에서 자체 애니메이션이 반복되도록 설정할 수 있습니다.

현재 애니메이션이 끝났을 때 다른 애니메이션을 호출할 수도 있습니다. 예를 들어 API 호출이 완료되면 버튼이 inProgress 상태에서 closed 상태로 바뀝니다.

애니메이션은 실제 처리 속도가 빨라지지 않아도, 최종 사용자가 동작이 더 빠르다고 느끼게 할 수 있습니다.

콜백을 디버깅 도구로 사용할 수도 있습니다. 예를 들어 console.warn()과 함께 사용해서 브라우저의 JavaScript 개발자 콘솔에서 애플리케이션의 진행 상태를 확인할 수 있습니다. 다음 코드는 open, closed 두 상태를 가진 원래 버튼 예제의 로그를 콘솔에 출력합니다.

open-close.ts

export class OpenClose {
  onAnimationEvent(event: AnimationEvent) {
    // openClose is trigger name in this example
    console.warn(`Animation Trigger: ${event.triggerName}`);

    // phaseName is "start" or "done"
    console.warn(`Phase: ${event.phaseName}`);

    // in our example, totalTime is 1000 (number of milliseconds in a second)
    console.warn(`Total time: ${event.totalTime}`);

    // in our example, fromState is either "open" or "closed"
    console.warn(`From: ${event.fromState}`);

    // in our example, toState either "open" or "closed"
    console.warn(`To: ${event.toState}`);

    // the HTML element itself, the button in this case
    console.warn(`Element: ${event.element}`);
  }
}

키프레임

여러 단계를 순서대로 실행하는 애니메이션을 만들려면 키프레임 을 사용하세요.

Angular의 keyframe() 함수는 하나의 시간 구간 안에서 스타일을 여러 번 변경할 수 있게 합니다. 예를 들어 버튼이 서서히 사라지는 대신, 2초 동안 색이 여러 번 바뀌게 할 수 있습니다.

키프레임

이 색상 변경 코드는 다음과 같이 작성할 수 있습니다.

status-slider.ts

transition('* => active', [
  animate(
    '2s',
    keyframes([
      style({backgroundColor: 'blue'}),
      style({backgroundColor: 'red'}),
      style({backgroundColor: 'orange'}),
    ]),
  ),

오프셋

키프레임에는 각 스타일 변경이 애니메이션의 어느 시점에 발생하는지 정의하는 offset을 지정할 수 있습니다. 오프셋은 애니메이션의 시작과 끝을 나타내는 0부터 1 사이의 상대값입니다. 한 번이라도 사용한다면 모든 키프레임 단계에 지정해야 합니다.

키프레임에 오프셋을 정의하는 것은 선택 사항입니다. 생략하면 일정한 간격의 오프셋이 자동으로 할당됩니다. 예를 들어 오프셋을 지정하지 않은 키프레임 세 개에는 0, 0.5, 1이 할당됩니다. 앞의 예제에서 중간 전환의 오프셋을 0.8로 지정하면 다음과 같이 동작합니다.

오프셋을 지정한 키프레임

오프셋을 지정하는 코드는 다음과 같습니다.

status-slider.ts

transition('* => active', [
  animate(
    '2s',
    keyframes([
      style({backgroundColor: 'blue', offset: 0}),
      style({backgroundColor: 'red', offset: 0.8}),
      style({backgroundColor: '#754600', offset: 1.0}),
    ]),
  ),
]),
transition('* => inactive', [
  animate(
    '2s',
    keyframes([
      style({backgroundColor: '#754600', offset: 0}),
      style({backgroundColor: 'red', offset: 0.2}),
      style({backgroundColor: 'blue', offset: 1.0}),
    ]),
  ),
]),

하나의 애니메이션에서 키프레임을 duration, delay, easing과 함께 사용할 수 있습니다.

키프레임으로 맥박 효과 만들기

애니메이션 전반의 특정 오프셋에 스타일을 정의하면, 키프레임으로 맥박처럼 뛰는 효과를 만들 수 있습니다.

키프레임으로 맥박 효과를 만드는 예제를 살펴봅시다:

  • 기존 open, closed 상태와 기존 높이, 색상, 불투명도의 변화는 1초 동안 진행됩니다.
  • 그 중간에 키프레임 시퀀스를 삽입해서 같은 1초 동안 버튼이 불규칙하게 맥박치는 것처럼 보이게 합니다.
불규칙한 맥박 효과를 만드는 키프레임

이 애니메이션 코드는 다음과 같이 작성할 수 있습니다.

open-close.ts

trigger('openClose', [
  state(
    'open',
    style({
      height: '200px',
      opacity: 1,
      backgroundColor: 'yellow',
    }),
  ),
  state(
    'close',
    style({
      height: '100px',
      opacity: 0.5,
      backgroundColor: 'green',
    }),
  ),
  // ...
  transition('* => *', [
    animate(
      '1s',
      keyframes([
        style({opacity: 0.1, offset: 0.1}),
        style({opacity: 0.6, offset: 0.2}),
        style({opacity: 1, offset: 0.5}),
        style({opacity: 0.2, offset: 0.7}),
      ]),
    ),
  ]),
]),

애니메이션을 적용할 수 있는 프로퍼티와 단위

Angular 애니메이션은 웹 애니메이션을 기반으로 하므로, 브라우저가 애니메이션을 지원하는 모든 프로퍼티에 애니메이션을 적용할 수 있습니다. 위치, 크기, 변형, 색상, 테두리 등이 여기에 포함됩니다. W3C는 CSS Transitions 페이지에서 애니메이션을 적용할 수 있는 프로퍼티 목록을 관리합니다.

숫자 값을 갖는 프로퍼티에는 적절한 접미사를 붙이고 따옴표로 감싼 문자열로 값을 전달해서 단위를 지정하세요:

  • 50픽셀: '50px'
  • 상대적인 글꼴 크기: '3em'
  • 백분율: '100%'

값을 숫자로 전달할 수도 있습니다. 이 경우 Angular는 기본 단위를 픽셀, 즉 px로 간주합니다. 50픽셀을 50으로 표현하는 것은 '50px'로 표현하는 것과 같습니다.

도움말: 반면 문자열 "50"은 유효한 값으로 간주하지 않습니다.

와일드카드로 프로퍼티 자동 계산하기

크기와 관련된 스타일 프로퍼티는 런타임이 되어야 값을 알 수 있는 경우가 있습니다. 예를 들어 엘리먼트의 너비와 높이는 콘텐츠나 화면 크기에 따라 달라지는 경우가 많습니다. 이런 프로퍼티는 CSS로 애니메이션을 적용하기 어려울 때가 많습니다.

이 경우 style() 안에서 특별한 와일드카드 프로퍼티 값 *를 사용할 수 있습니다. 해당 스타일 프로퍼티 값은 런타임에 계산된 뒤 애니메이션에 적용됩니다.

다음 예제에는 HTML 엘리먼트가 페이지에서 퇴장할 때 사용하는 shrinkOut 트리거가 있습니다. 애니메이션은 엘리먼트가 퇴장하기 전의 높이를 가져와, 그 높이에서 0까지 줄어들도록 적용됩니다.

hero-list-auto.ts

animations: [
  trigger('shrinkOut', [
    state('in', style({height: '*'})),
    transition('* => void', [style({height: '*'}), animate(250, style({height: 0}))]),
  ]),
],

키프레임 정리

Angular의 keyframes() 함수를 사용하면 하나의 전환 안에 여러 중간 스타일을 지정할 수 있습니다. 선택 사항인 offset으로 각 스타일 변경이 애니메이션의 어느 시점에 일어나야 하는지 정의할 수 있습니다.

Angular 애니메이션 더 알아보기

다음 문서도 참고해 보세요: