[Angular] ngFor에 필요한 trackBy Directive로 만들기
trackBy?
Angular에서 특정 배열에 있는 요소를 반복적으로 DOM에 출력하고 싶을 때 ngFor를 사용한다.
ngFor는 간편하게 사용할 수 있지만, 아래와 같은 단점이 존재한다.
- ngFor를 사용하는 배열이 변경 될 경우 모든 DOM이 Refresh 되면서 오버헤드가 발생한다.
이를 개선하기 위해 trackByFunction을 만들어야 한다.
이 trackByFunction의 역할은 아래와 같다.
- trackBy에 걸려있는 값에 속해있는 요소가 변경되었을 경우에만 Refresh가 발생된다.
즉, Primary key를 정해 trackBy로 잡아주면, 배열의 요소가 변경되었을 때 변경된 것만 Refresh 할 수 있다는 것이다.
위 문장에서 유추할 수 있겠지만, 당연히도 trackBy에는 각 배열의 요소가 갖고 있는 고유의 값을 사용해야 한다.
이 때 배열에 있는 index를 사용하는 것은 올바르지 않다.(경우에 따라 다르긴 하나 일반적으로 사용되지는 않는다.)
왜냐하면 index를 Primary Key로 잡게 되면 배열이 정렬되거나, 하나의 요소가 사라져 index가 다시 재배치될 때 모든 DOM이 Refresh 될 수 있기 때문이다.
따라서 배열의 요소 안에 'id' 와 같은 Primary Key를 부여하고 사용하는 것이 좋다.
trackBy의 일반적인 사용 방법은 아래와 같다.
// [*].component.ts
let items = [
{ id: 1, name: 'first'},
{ id: 2, name: 'second'},
{ id: 3, name: 'third'},
{ id: 4, name: 'fourth'}
];
trackByItem = (index: number, item: any): number => item.id;
<!-- [*].component.html -->
<div *ngFor="let item of items;trackBy: trackByItem"></div>
즉, items 배열 안에 있는 id가 속해있는 요소가 변경되었을 때만 다시 Refresh 하겠다는 것이다.
trackBy의 문제점
사실 문제점이라기 보다는 개발자들이 개발을 하며 반복적인 코드를 보게되거나, 너무 반복적이다 보니 개발자가 빠뜨리고 안넣었을 때의 문제점이 있다.
이는 앱 성능에 문제를 일으킬 수 있다.
매번 Components의 TypeScript에 trackByItem 함수를 추가하는 것은 보기에도 좋지 않고, 개발자 경험 측면에서도 좋지 않다.
trackBy, 어떻게 동작할까?
Directive로 만들기 전 trackBy가 어떻게 동작하는지에 대해 파악하고 있어야 만들 수 있으니 분석해보도록 하자.
위 작성한 ngFor에 trackBy를 사용한 HTML을 ng-template으로 풀어보면 아래와 같은 결과가 나온다.
<ng-template ngFor let-item [ngForOf]="items" [ngForTrackBy]="trackByItem"></ng-template>
ngFor Directive에서 사용할 배열의 이름은 [ngForOf] 에 들어가게 되고, trackBy 의 원래 이름은 [ngForTrackBy] 인 것을 알 수 있다.
일반적인 방법으로 Directive를 만들었을 때의 문제점
일반적인 방법으로 Directive를 만든다 했을 때는 아래와 같은 결과물이 나오게 된다.
<div *ngFor="let item of items" [trackByFn]="'id'"></div>
위 코드를 ng-template으로 풀어보자.
<ng-template ngFor let-item [ngForOf]="items">
<div [ngForTrackByFn]="'id'"></div>
</ng-template>
ngFor에 속한 것이 아닌, div에 속해 있어 ngFor에 관여할 수 없게 된다.
이러면 만들고자 했던 기능을 구현할 수 없다.
그러면 어떻게 만들어야 할까?
위 코드를 작성함으로써 우리는 일단 무엇이 문제인지는 알고 있다.
바로, 내가 만든 Custom Directive가 ngFor에 관여할 수 있어야 한다는 것이다.
그러기 위해서는 @Host Decoreate를 사용해야 한다.
@Host() 를 이용하여 trackBy Directive를 만들어보자.
위에 작성한 바와 같이 Components의 Template과 연관된 DI인 'ngFor'를 가져와 Directive를 만들 것이다.
그리고 구현하려고 하는 trackBy Directive를 만들기 위해 ngFor에 속한 ngForTrackBy 함수에 우리가 원하는 필드 값을 주입할 것이다.
import { NgForOf } from "@angular/common";
import { Directive, Host, Input } from "@angular/core";
@Directive({
selector: "[ngForTrackByField]",
})
export class NgForTrackByFieldDirective<T> {
@Input()
public ngForTrackByField!: keyof T;
constructor(@Host() private ngFor: NgForOf<T>) {
this.ngFor.ngForTrackBy = (index: number, item: T) => {
return this.ngForTrackByField ? item[this.ngForTrackByField] : item;
};
}
}
위 코드는 생성자로 ngFor를 가져오게 되어 있고, ngFor의 ngForTrackBy의 함수를 직접 구현한 것이다.
위 Directive를 사용한 모습은 아래와 같다.
<div *ngFor="let item of items; trackByField: 'id'"></div>
이로써 trackByFunction을 매번 같은 컴포넌트에 만들지 않고도 trackByFunction을 쉽게 사용할 수 있게 되었다.
만약, id와 같이 고유한 값이 없거나, 겹칠 가능성이 있어 배열로 만들어야 한다면 위 코드는 수정되어야 한다.
배열로 만들어 문자열을 이어 붙이는 형식으로 만들면 될 것이다.