trackBy Directive 만들기

Adam Kim·2025년 10월 10일
0

angular

목록 보기
48/88

Angular에서 리스트를 렌더링할 때, 배열의 데이터가 변경되면 Angular는 DOM 전체를 다시 렌더링할 수 있습니다. 이는 성능 저하의 원인이 될 수 있습니다. 이 문제를 해결하기 위해 Angular는 각 아이템을 고유하게 식별하여 변경된 부분만 업데이트하는 메커니즘을 제공합니다.

최신 Angular의 해결책: @for와 track

Angular v17부터 도입된 새로운 Control Flow 문법인 @for는 이 문제를 매우 간단하게 해결합니다. track 키워드를 사용하여 각 아이템을 추적할 고유한 프로퍼티를 직접 지정할 수 있습니다.

<ul>
  @for (item of data(); track item.id) {
    <li>{{ item.name }}</li>
  }
</ul>

track item.id 구문은 trackBy 함수의 필요성을 완전히 없애줍니다. 새로운 프로젝트나 최신 Angular 버전을 사용한다면, 항상 @for와 track을 사용하는 것이 가장 좋은 방법입니다.

ngFor에 연결된 배열의 값이 변경될 때 refresh가 발생하여 rendering이 일어나는데, 이를 방지하기 위해 trackBy를 사용합니다.

즉, trackBy를 사용하면 값이 변경되어도 rendering이 되지 않으므로 성능을 향상 시킬 수 있습니다.

기존 *ngFor를 위한 trackBy Directive 만들기

하지만 레거시 프로젝트나 특정 라이브러리 호환성 문제로 여전히 ngFor를 사용해야 하는 경우가 있습니다. ngFor에서 성능을 최적화하려면 trackBy 프로퍼티와 함께 추적 함수(trackByFn)를 사용해야 합니다.

{% raw %}
<!-- app.component.html -->
<div *ngFor="let item of data; trackBy: trackByFn"></div>
{% endraw %}
// app.component.ts
...
trackByFn = (item): number => item.id

trackBy의 단점

trackBy의 단점은 위의 코드에서 보듯 ngFor문이 사용될 때마다 component에 매번 trackByFn 함수를 만들어주어야 한다는 것입니다.

이 불편함 때문에 개발자들이 종종 trackBy를 고의 또는 실수로 누락하는 경우가 있으며 이는 제품의 성능에 영향을 미칩니다.

trackBy 동작방식

목표인 trackBy Directive를 만들기 위해 먼저 trackBy가 어떻게 동작하는지 원리를 알아봅시다.

trackBy를 좀 더 자세히 파악하기 위해 ng-template 으로 변경해보겠습니다.

아래의 두 코드는 동일한 결과를 갖습니다.

{% raw %}
<div *ngFor="let item of data; trackBy: trackByFn"></div>

<ng-template ngFor let-item [ngForOf]="data" [ngForTrackBy]="trackByFn"></ng-template>
{% endraw %}

ng-template에서 보면 알 수 있듯이 trackBy의 원래 이름은 ngForTrackBy이며 ngFor directive의 전달인자로 볼 수 있습니다.

trackBy를 Directive로 만들 때 문제점

어떻게 ngForTrackBy를 directive화 할 수 있을까요? 일반적인 방법으로 생각해보면 다음과 같을 것 입니다.

{% raw %}
<div *ngFor="let item of data" [trackByFn]="'id'"></div>
{% endraw %}

이를 ng-template에서 풀어보면 다음과 같이 됩니다.

{% raw %}
<ng-template ngFor let-item [ngForOf]="data">
	<div [ngForTrackByFn]="'id'"></div>
</ng-template>
{% endraw %}

즉, 일반적인 방법으로 directive로 만들경우 ngFor directive의 전달인자가 아닌 별도의 directive로 인식되므로 ngFor에 관여할 수 없게 되므로 우리가 원하는 기능을 구현할 수 없습니다.

그러면 어떻게 ngFor에 접근할 수 있을까요?

Host를 활용한 ngForTrackBy Directive 만들기

이를 구현하기 위해 @Host 함수를 소개합니다.

@Host는 Angular의 내장된 기능으로 @Self와 유사하게 연관된 DI를 가져올 수 있습니다.

차이가 있다면, @Self는 component와 연관된 DI를 가져오는 반면 component의 template과 viewProviders에 연관된 DI 를 가져옵니다.

즉, 우리는 @Host를 사용하여 ngFor를 가져오고 ngFor에 속한 ngForTrackBy 함수에 우리가 원하는 필드 값을 주입해야 합니다.

이 때, @HostNgForOf를 지정하여 ngFor에 접근할 수 있도록 할 예정이며, 코드는 아래와 같습니다.

// ng-for-track-by-field.directive.ts
import { NgForOf } from '@angular/common';
import { Directive, Input, inject } from '@angular/core';

@Directive({
  // ngFor와 함께 사용될 때만 활성화되도록 셀렉터 구성
  selector: '[ngFor][ngForTrackByField]',
  standalone: true,
})
export class NgForTrackByFieldDirective<T extends Record<string, any>> {
  @Input('ngForTrackByField')
  public field!: keyof T;

  // host 요소에 있는 NgForOf 인스턴스를 주입받습니다.
  private ngFor = inject(NgForOf<T>, { host: true });

  constructor() {
    // NgForOf 디렉티브의 trackBy 함수를 동적으로 설정합니다.
    this.ngFor.ngForTrackBy = (index: number, item: T) => {
      // field가 지정되었다면 해당 프로퍼티 값을, 아니면 아이템 자체를 반환합니다.
      return this.field ? item[this.field] : item;
    };
  }
}

위의 코드에서 보면 ngForTrackByFieldngFor를 호스트로 두는 directive이며, 호출되었을 때 ngForngForTrackBy함수에 원하는 값을 주입하고 실행시키는 역할을 합니다.

이제 template에서는 trackByField를 통해 간단히 trackBy를 구현할 수 있습니다.

디렉티브 사용하기

이제 템플릿에서 trackByFn 함수 없이 간단하게 trackBy를 구현할 수 있습니다.

// app.component.ts
import { Component, signal } from '@angular/core';
import { CommonModule } from '@angular/common'; // *ngFor를 위함
import { NgForTrackByFieldDirective } from './ng-for-track-by-field.directive';

@Component({
  selector: 'app-root',
  standalone: true,
  imports: [CommonModule, NgForTrackByFieldDirective], // 디렉티브 import
  template: `
    <h3>*ngFor with Custom trackBy Directive</h3>
    <div *ngFor="let item of data(); ngForTrackByField: 'id'">
      {{ item.name }}
    </div>
  `,
})
export class AppComponent {
  data = signal([ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ]);
  // 더 이상 trackByFn 함수가 필요 없습니다!
}

참고 사이트

profile
Angular2+ Developer

0개의 댓글