마스터-세부 사항 그리드 레이아웃에서 주문형 데이터 로드
주문형 데이터 로딩은 마스터-세부 정보 인터페이스를 확장하기 위한 강력한 기술입니다. 모든 세부 정보를 미리 가져오는 대신 사용자가 필요할 때만 관련 레코드를 로드합니다. 이 문서에서 자세히 알아보세요.
이전 기사에서 Ignite UI for Angular Grid를 사용하여 마스터-디테일 레이아웃 시작 하기에서는 깔끔하고 효율적인 마스터-디테일 인터페이스를 설정하는 방법을 살펴보았습니다. 이 패턴은 화면을 어지럽히지 않고 항목이 있는 주문이나 직원이 있는 부서와 같은 관련 레코드를 표시하는 데 이상적입니다.
하지만 데이터 세트가 방대하면 어떻게 될까요? 모든 레코드에 대한 모든 세부 정보를 한 번에 로드하는 것은 비효율적입니다. 주문형 데이터 로딩이 필요한 곳입니다. 가능한 모든 세부 정보를 미리 가져오는 대신 사용자가 행을 확장할 때만 데이터를 로드하므로 초기 렌더링 속도가 빨라지고 상호 작용이 원활해지며 확장성이 향상됩니다.
이 기사에서는 주문형 로딩이 중요한 이유, Ignite UI for Angular 그리드 구성 요소에서 이를 구현하는 방법, 이를 최대한 활용하기 위한 모범 사례에 대해 자세히 설명합니다.
마스터-세부 사항 템플릿 확장
첫 번째 기사를 읽었다면 확장 가능한 행과 세부 템플릿으로 마스터 그리드를 설정하는 방법과 데이터를 표시할 위치를 이미 알고 있을 것입니다.
- 동일한 데이터 세트 – 예를 들어 주문 및 관련 항목이 함께 번들로 제공됩니다
- 외부 데이터 세트 – 예를 들어 customerID를 파라미터로 사용하고 API를 호출하여 고객 주문 데이터를 검색하는 서비스입니다.
대규모 데이터 세트의 경우 두 번째 접근 방식(외부 세부 정보 가져오기)은 주문형 로딩이 가장 효과적인 것으로 입증되는 곳입니다.
Angular 프로젝트 설정
주문형 로딩 구현을 시작하기 전에 데모를 위한 Angular 작업 공간을 준비해 보겠습니다. 단계는 다음과 같습니다.
- 새 Angular 프로젝트 만들기
아직 없는 경우 Angular CLI를 사용하여 새 Angular 애플리케이션을 생성하고 선호하는 IDE에서 프로젝트를 엽니다.
ng new load-on-demand-grid cd load-on-demand-grid
- Install Ignite UI for Angular
풍부한 UI 구성 요소 세트를 제공하는 Ignite UI for Angular 구성 요소 라이브러리, 가장 중요한 것은 마스터-세부 정보 및 주문형 로딩 예제의 기반인 igxGrid를 사용할 것입니다. 다음을 사용하여 프로젝트에 추가합니다.
ng add igniteui-angular
- Create a demo component
모든 것을 체계적으로 유지하려면 그리드 데모를 빌드할 전용 Angular 구성 요소를 생성합니다.
ng generate component pages/grid-demo
- 데모에 대한 라우팅 설정
마지막으로 데모 구성 요소가 기본 보기가 되도록 라우팅 구성을 조정합니다. 기존 app.routes.ts(또는 라우팅 모듈)을 최소한의 설정으로 교체하십시오.
import { Routes } from '@angular/router';
export const routes: Routes = [
{ path: '', redirectTo: '/demo', pathMatch: 'full' },
{ path: 'demo', loadComponent: () => import('./pages/grid-demo/grid-demo.component').then(m => m.GridDemoComponent) },
{ path: '**', redirectTo: '/demo' }
];
app.config.ts의 공급자에 provideHttpClient를 추가합니다.
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideAnimations(),
provideHttpClient()
]
};
데이터 설정
마스터-세부 사항 그리드에서 주문형 로딩을 구현하기 전에 신뢰할 수 있는 데이터 계층이 필요합니다. 데이터 모델 및 서비스를 준비하면 그리드에 의미 있는 콘텐츠가 표시되고 사용자가 행과 상호 작용할 때 관련 세부 정보를 가져올 수 있습니다.
IgxGrid에 대한 데이터는 다양한 소스에서 가져올 수 있습니다.
- 정적/로컬 데이터 – 자산 폴더의 JSON 파일입니다.
- 원격 API – 백엔드 서비스에서 Angular의 HttpClient를 통해 가져옵니다.
- 모의 데이터 – 개발 중에 서비스에서 직접 생성됩니다.
이 예제에서는 고객 및 관련 주문을 제공하는 사용자 지정 원격 Northwind Swagger API에 연결합니다. 개념은 데이터 소스 구조에 관계없이 동일하게 유지됩니다.
Define Data Models
유형 안전성과 명확성을 적용하려면 전용 models.ts 파일에서 데이터를 나타내는 TypeScript 인터페이스를 정의합니다. 이러한 모델은 API 응답의 구조를 미러링하고 데이터를 그리드에 쉽게 바인딩할 수 있도록 합니다.
export interface AddressDto {
street?: string;
city?: string;
region?: string;
postalCode?: string;
country?: string;
phone?: string;
}
export interface CustomerDto {
customerId?: string;
companyName: string;
contactName?: string;
contactTitle?: string;
address?: AddressDto;
}
export interface OrderDto {
orderId: number;
customerId: string;
employeeId: number;
shipperId?: number;
orderDate?: Date;
requiredDate?: Date;
shipVia?: string;
freight: number;
shipName?: string;
completed: boolean;
shipAddress?: AddressDto;
}
Create a Data Service
다음으로, 모든 API 호출을 전용 서비스(예: northwind-swagger.service.ts)로 중앙 집중화합니다. 또한 앱의 복원력을 유지하기 위해 기본 오류 처리를 추가합니다.
const API_ENDPOINT = 'https://data-northwind.indigo.design';
@Injectable({
providedIn: 'root'
})
export class NorthwindSwaggerService {
constructor(
private http: HttpClient
) { }
public getCustomerDtoList(): Observable<CustomerDto[]> {
return this.http.get<CustomerDto[]>(`${API_ENDPOINT}/Customers`)
.pipe(catchError(this.handleError<CustomerDto[]>('getCustomerDtoList', [])));
}
public getOrderDtoList(id: string): Observable<OrderDto[]> {
return this.http.get<OrderDto[]>(`${API_ENDPOINT}/Customers/${id}/Orders`)
.pipe(catchError(this.handleError<OrderDto[]>('getOrderDtoList', [])));
}
private handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
console.error(`${operation} failed: ${error.message}`, error);
return of(result as T);
};
}
}
Bind the Grid to Customer Data
모델과 서비스가 준비되었으므로 다음 단계는 그리드에 고객 데이터 세트를 표시하는 것입니다. 그리드의 데이터 속성은 서비스에서 데이터를 가져와 구성 요소가 초기화될 때 채워지는 northwindSwaggerCustomerDto 배열에 바인딩됩니다.
<igx-grid [data]="northwindSwaggerCustomerDto" primaryKey="customerId" [allowFiltering]="true" filterMode="excelStyleFilter" class="grid"> <igx-column field="customerId" dataType="string" header="customerId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> <igx-column field="companyName" dataType="string" header="companyName" [filterable]="true" [sortable]="true" required="true" [maxlength]="100" [selectable]="false"></igx-column> <igx-column field="contactName" dataType="string" header="contactName" [filterable]="true" [sortable]="true" [maxlength]="50" [selectable]="false"></igx-column> </igx-grid>
TypeScript 쪽에서 GridDemoComponent 클래스에 서비스 구독을 추가하여 고객 데이터를 검색하고 northwindSwaggerCustomerDto 속성에 저장합니다. 그런 다음, 그리드 데이터 입력 속성을 'northwindSwaggerCustomerDto '에 바인딩합니다.
@Component({
selector: 'app-grid-demo',
standalone: true,
imports: [IgxCheckboxComponent, IgxSimpleComboComponent, IgxGridComponent, IgxColumnComponent, IgxColumnMaxLengthValidatorDirective, NgIf, AsyncPipe, IgxGridDetailTemplateDirective],
templateUrl: './grid-demo.component.html',
styleUrl: './grid-demo.component.css'
})
export class GridDemoComponent implements OnInit, OnDestroy{
private destroy$ = new Subject<void>();
public northwindSwaggerCustomerDto: CustomerDto[] = [];
constructor(private northwindSwaggerService: NorthwindSwaggerService) {}
ngOnInit() {
this.northwindSwaggerService
.getCustomerDtoList()
.pipe(takeUntil(this.destroy$))
.subscribe(data => (this.northwindSwaggerCustomerDto = data));
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

세부 템플릿 설계 및 주문형 로드 구현
마스터 그리드가 많은 상위 행(고객)을 표시하는 경우 모든 행에 대해 모든 관련 하위 목록(주문)을 미리 가져오는 것은 비효율적입니다. 대신 사용자가 행을 확장할 때만 세부 정보를 로드합니다. 이것이 주문형 로딩의 본질입니다: 필요할 때만 하위 데이터를 요청한 다음 세부 템플릿에 표시합니다.
어떻게 작동하나요?
- igxGridDetail 템플릿을 마스터 igx-grid 안에 넣습니다.
- 해당 템플릿에서 Observable<OrderDto[]>를 반환하는 메서드(예: getOrders(customerId))를 호출합니다.
- 비동기 파이프를 사용하여 템플릿에서 관찰 가능 항목을 구독하면 사용 가능한 결과를 렌더링 Angular 있습니다.
- 여러 확장(또는 빠른 변경 감지 주기)이 새 HTTP 호출을 트리거하지 않도록 각 고객의 관찰 가능/결과를 캐시합니다.
어떻게 구현합니까?
- 세부 템플릿 추가
먼저 igxGridDetail 지시문과 함께 ng-template을 추가하여 세부 정보 콘텐츠에 대한 자리 표시자를 정의합니다. 이렇게 하면 확장된 각 행의 세부 보기가 렌더링되는 위치를 표시합니다.
<ng-template igxGridDetail let-rowData></ng-template>
- 주문에 바인딩된 콤보 추가(요청 시 로드됨)
템플릿 내에서 원하는 구성 요소를 렌더링할 수 있습니다. 이 예에서는 선택한 고객에 대한 주문을 표시하는 콤보 상자를 추가합니다. 여기서 핵심은 행이 확장될 때까지 주문이 로드되지 않는다는 것입니다.
<ng-container *ngIf="getOrders(rowData.customerId) | async as orders">
<div class="row-layout group">
<igx-simple-combo
type="border"
[data]="orders"
displayKey="orderId"
class="single-select-combo">
<label igxLabel>Order Id</label>
</igx-simple-combo>
</div>
</ng-container>
행이 처음 확장되면 getOrders(customerId) 메서드가 HTTP 호출을 수행하고 관찰 가능 항목을 반환합니다. 비동기 파이프를 사용하기 때문에 Angular 자동으로 구독하고 데이터가 도착하면 콤보를 렌더링합니다.
여기서 매우 중요한 참고 사항은 ng-template이 있으면 Angular 매 프레임(변경 감지)이 템플릿에 데이터를 계속 로드한다는 것입니다. 이 데이터가 요청에서 로드되는 경우 이는 애플리케이션이 필연적으로 정지될 때까지 표시되는 모든 템플릿에 대해 매초 몇 개의 요청이 이루어진다는 것을 의미합니다. 이것이 가져오기 후 데이터를 캐시해야 하는 이유입니다.
private ordersCache = new Map<string, Observable<OrderDto[]>>();
getOrders(customerId: string): Observable<OrderDto[]> {
if (!this.ordersCache.has(customerId)) {
const request$ = this.northwindSwaggerService
.getOrderDtoList(customerId)
.pipe(take(1), shareReplay(1));
this.ordersCache.set(customerId, request$);
}
return this.ordersCache.get(customerId)!;
}

여기서 몇 가지 일이 일어나고 있습니다.
- customerId로 키가 지정된 Map에 관찰 가능 항목을 캐시합니다. 이렇게 하면 행에 대한 데이터를 가져오면 새 HTTP 호출을 트리거하는 대신 동일한 관찰 가능 항목이 재사용됩니다.
- take(1)는 첫 번째 응답 후에 관찰 가능 항목이 완료되도록 보장하여 구독 관리를 깔끔하게 만들고 메모리 누수를 방지합니다.
- shareReplay(1)는 여러 구독자(또는 변경 감지 주기)가 동일한 데이터를 요청하는 경우 하나의 HTTP 요청만 전송되도록 합니다. 또한 늦은 구독자에게 결과를 재생합니다(예: 행이 축소되었다가 다시 확장될 때).
- 그런 다음 데이터를 ordersCache에 저장하고 반환합니다
캐싱과 shareReplay를 결합하면 각 고객 행이 몇 번 확장되거나 변경 감지를 실행하는 빈도에 관계없이 최대 하나의 요청 Angular 트리거되도록 합니다.
선택한 주문에 대한 세부 정보 표시
지금까지 고객 행을 확장하고 주문형 주문 목록을 가져오는 방법을 보여 주었습니다. 다음으로, 사용자가 해당 목록에서 특정 주문을 선택하고 행 바로 아래에 세부 정보를 표시하도록 합니다.
- 콤보에 선택 이벤트 추가
새 주문이 선택될 때마다 알려주기 위해 콤보 상자를 확장합니다. 이것은 selectionChanging 이벤트를 처리하여 수행됩니다.
<igx-simple-combo
type="border"
[data]="orders"
displayKey="orderId"
(selectionChanging)="onOrderSelectionChange(rowData.customerId, $event.newValue)"
class="single-select-combo"
>
<label igxLabel>Order Id</label>
</igx-simple-combo>
- 고객당 선택한 주문 추적
Map<string OrderDto>를 사용하여 customerId로 키가 지정된 선택 항목을 추적합니다. 이렇게 하면 확장된 각 행이 선택한 순서를 기억합니다.
public selectedOrders = new Map<string, OrderDto>();
onOrderSelectionChange(customerId: string, order: OrderDto) {
this.selectedOrders.set(customerId, order);
}
getSelectedOrder(customerId: string): OrderDto | undefined {
return this.selectedOrders.get(customerId);
}
- Show order details
마지막으로 주문이 선택되면 확장된 템플릿에서 세부 정보를 렌더링할 수 있습니다. getSelectedOrder(customerId)를 호출하여 저장된 선택 항목을 검색하고 해당 필드를 표시합니다.
<div *ngIf="getSelectedOrder(rowData.customerId) as selectedOrder" class="column-layout group_1">
<h6 class="h6">Order Details</h6>
<div class="row-layout group_2">
<div class="column-layout group_3">
<div class="row-layout group_4"><p class="text">Completed</p></div>
<div class="row-layout group_4"><p class="text">Shipper</p></div>
<div class="row-layout group_4"><p class="text">Order Date</p></div>
<div class="row-layout group_4"><p class="text">Country</p></div>
<div class="row-layout group_4"><p class="text">City</p></div>
<div class="row-layout group_4"><p class="text">Street</p></div>
<div class="row-layout group_5"><p class="text">Postal Code</p></div>
</div>
<div class="column-layout group_6">
<div class="row-layout group_7"><igx-checkbox [checked]="!!selectedOrder?.completed" class="checkbox"></igx-checkbox></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipperId }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.orderDate }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.country }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.city }}</p></div>
<div class="row-layout group_4"><p class="text">{{ selectedOrder?.shipAddress?.street }}</p></div>
<div class="row-layout group_5"><p class="text_1">{{ selectedOrder?.shipAddress?.postalCode }}</p></div>
</div>
</div>
</div>
</div>
이를 통해 간단하면서도 기능적인 세부 정보 보기를 제공합니다: 사용자가 고객 행을 확장하면 콤보에서 주문을 선택할 수 있으며 주문 세부 정보가 바로 아래에 표시됩니다.

Cache Invalidation & Refresh Strategies
만료되지 않는 캐시는 부실해지거나 무제한으로 증가할 수 있습니다. 앱에 맞는 전략을 구현하도록 선택할 수 있습니다.
Manual Refresh
캐시 항목을 대체하는 refreshOrders(customerId)를 호출하는 세부 정보 템플릿에 "새로 고침" 단추를 추가합니다.
refreshOrders(customerId: string) {
const request$ = this.northwindSwaggerService.getOrderDtoList(customerId).pipe(take(1), shareReplay(1));
this.ordersCache.set(customerId, request$);
}
축소 시 지우기
행이 축소될 때 캐시된 관찰 가능 항목을 지웁니다. 이렇게 하면 다음에 사용자가 확장할 때 새로운 요청이 보장됩니다.
<igx-grid (rowToggle)="onRowToggle($event)"...>
onRowToggle(event: IRowToggleEventArgs) {
if (this.ordersCache.size > 0 && this.ordersCache.has(event.rowID)) {
this.ordersCache.delete(event.rowID);
}
}
마스터 그리드를 설정하고, 주문형 세부 정보 로드를 구현하고, 중복 요청을 방지하기 위해 결과를 캐시하고, 새로 고침 전략에 대해서도 논의했습니다.
이제 이 패턴을 더 큰 데이터 세트 및 프로덕션 시나리오로 확장하는 데 도움이 되는 추가 고려 사항과 모범 사례를 살펴보겠습니다.
Handling Large Detail Data
세부 데이터 세트 자체가 큰 경우 모든 것을 한 번에 가져오고 렌더링하면 성능이 저하될 수 있습니다. 대신 다음 전략 중 하나를 사용합니다.
- 클라이언트 또는 서버 측 페이징: page 및 pageSize 매개변수를 주문 API에 전달하여 한 번에 데이터 조각만 가져옵니다.
- 주문형 로드: 현재 필요한 데이터만 가져옵니다.
메모리 및 수명 주기 고려 사항
- 메모리 누수를 방지하기 위해 ngOnDestroy()에서 캐시를 지웁니다: this.ordersCache.clear().
- 관찰 가능 항목뿐만 아니라 캐시된 데이터를 유지하는 경우 총 메모리를 제한하는 것이 좋습니다.
- Avoid storing large, nested object Maps indefinitely.
사전 로드된 세부 정보와의 비교
| Approach | 최고 | 단점 |
|---|---|---|
| Preloaded details | 작은 데이터 세트, 간단한 데모 및 빠른 프로토타입 | 초기 로드 속도가 느리고 메모리 사용량이 증가합니다. |
| On-demand loading | 대규모/복잡한 데이터 세트 및 프로덕션 앱 | 코드 복잡성이 약간 더 높고 비동기 처리가 필요합니다. |
결론
주문형 데이터 로드는 마스터-세부 사항 그리드 레이아웃의 자연스러운 확장입니다. 성능을 향상시키고, 대규모 데이터 세트로 원활하게 확장되며, 원활한 사용자 경험을 보장합니다.
이 패턴을 처음 사용하는 경우 먼저 Ignite UI for Angular 그리드를 사용하여 마스터-세부 정보 레이아웃 시작하기 문서를 확인하세요. 그런 다음 자체 앱에 주문형 로딩을 적용하여 데이터 그리드를 한 단계 더 발전시키세요.
Ignite UI for Angular를 사용하면 데이터가 증가하더라도 빠르게 유지되는 반응형 데이터 기반 애플리케이션을 구축할 수 있습니다.
전체 주문형 로드 앱 샘플을 확인하세요.