Angular에서 Directive를 사용해서 무한 스크롤을 구현해보겠다.
무한 스크롤이란?
유저에게 대용량 데이터를 한번에 가져와서 보여주려면 많은 시간이 걸린다.
그래서 유저의 화면 영역을 채우는 데이터를 불러와 보여주고 스크롤을 내리면 그만큼의 데이터를 불러와서 보여주는 것을 의미한다.
1. 데이터를 가져와야 하는 시점 계산하기
무한스크롤을 구현하기 위해서는 스크롤의 위치를 계산하는 것이 중요하다.
문서의 높이와 현재 화면의 높이 등을 계산해서 데이터를 더 가져올 지를 선택해야 하기 때문이다.
- scrollHeight: 전체 문서의 높이
- innerHeight or clientHeight : 뷰포트의 높이
- scrollTop : 문서의 최상단부터 뷰포트의 최상단까지의 거리
|--------------------| <- 페이지 최상단 (scrollTop = 0)
| |
| 스크롤된 부분 |
| (500px) |
|--------------------| <- 현재 뷰포트의 최상단 (scrollTop = 500)
| |
| 뷰포트 내용 | <- 현재 유저가 보고 있는 부분
| (800px 높이) |
| |
|--------------------| <- 뷰포트의 하단 (scrollTop + clientHeight)
| |
| |
|--------------------| <- 페이지의 하단
그리고 유저의 scroll 이벤트를 감지해, 위 내용을 바탕으로 데이터를 가져와야 하는 시점을 계산하는 Angular Directive를 생성해야 한다.
이때, 데이터를 불러오는 시점은 전체 문서의 높이(scrollHeight)에서 뷰포트의 높이(innerHeight or clientHeight)와 문서가 스크롤 된 부분(scrollTop)의 합을 뺀 값과 임계값을 비교해서 이벤트를 방출시키면 된다.
이때, 임계값(threshold)은 스크롤이 문서의 끝의 얼마나 가까워졌는지를 결정하는 값을 의미한다.
@Directive({
selector: '[appScrollNearEnd]'
})
export class ScrollNearEndDirective {
@Input() threshold = 50;
@Output() nearEnd = new EventEmitter<void>();
constructor(private el: ElementRef) { }
@HostListener('window:scroll', ['$event.target'])
public windowScrollEvent() {
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = window.innerHeight || document.documentElement.clientHeight;
const scrollPosition = scrollTop + clientHeight;
if (scrollHeight - scrollPosition < this.threshold) {
this.nearEnd.emit();
}
}
}
2. 무한 스크롤을 컴포넌트에서 구현하기
무한 스크롤이 필요한 <div>부분에서 Directive를 선언하고 방출한 nearEnd 이벤트를 해당 컴포넌트에서 받으면 된다. 이 이벤트일 때, 데이터를 로드해주면 된다.
<div #scrollTable appScrollNearEnd (nearEnd)="onNearEndScroll()">
<table
mat-table
[dataSource]="dataSource"
>
...
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
@Component({
selector: 'app-review-list',
templateUrl: './review-list.component.html',
styleUrl: './review-list.component.scss'
})
export class ReviewListComponent implements OnInit {
private readonly httpService = inject(HttpService);
public page: number = 0;
public isLoading: boolean = true;
public dataSource: IReview[] = [];
public displayedColumns: string[] = ['id', 'firstName', 'lastName', 'age'];
ngOnInit() {
this.loadMoreData();
}
loadMoreData() {
this.isLoading = true;
this.httpService.GetJson(`./assets/json/table${this.page}.json`).subscribe(response => {
this.dataSource = [...this.dataSource, ...response];
this.page++;
const scrollTop = document.documentElement.scrollTop;
const scrollHeight = document.documentElement.scrollHeight;
const clientHeight = window.innerHeight || document.documentElement.clientHeight;
const scrollPosition = scrollTop + clientHeight;
if (scrollHeight <= scrollPosition) {
this.loadMoreData();
}
this.isLoading = false;
});
}
onNearEndScroll(): void {
if (!this.isLoading) {
this.loadMoreData();
}
}
}