내용으로 건너뛰기
Ignite UI for Angular 그리드를 사용하여 마스터-세부 사항 레이아웃 시작

Ignite UI for Angular 그리드를 사용하여 마스터-세부 사항 레이아웃 시작

마스터-디테일 레이아웃은 화면을 압도하지 않고 관련 데이터를 표시할 수 있는 간단하고 확장 가능하며 사용자 친화적인 방법을 제공하는 입증된 UI 패턴입니다. 이 문서에서 자세히 알아보세요.

12min read

엔터프라이즈 애플리케이션(예: CRM, ERP 및 관리 대시보드)을 구축할 때 하나의 레코드가 여러 관련 레코드에 연결된 관계형 데이터 세트로 작업하는 것이 일반적입니다. 예를 들어, 주문에 다양한 품목이 있을 수 있고, 고객에게 여러 트랜잭션이 있을 수 있으며, 부서에 직원이 다수 있을 수 있습니다.

마스터-세부 사항 레이아웃은 이러한 시나리오에 대해 입증된 UI 패턴입니다. 화면을 압도하지 않고 관련 데이터를 표시할 수 있는 간단하고 확장 가능하며 사용자 친화적인 방법을 제공합니다.

이 기사에서는 Ignite UI for Angular를 사용하여 깔끔하고 효율적인 마스터-디테일 인터페이스를 구축하는 방법을 안내합니다. 강력한 그리드 구성 요소와 igxGridDetail을 사용하여 중첩된 템플릿에 대한 기본 제공 지원을 살펴보면 최소한의 설정과 고성능으로 계층적 데이터를 표시할 수 있습니다.

마스터-세부 사항 레이아웃이란 무엇입니까?

마스터-세부 사항 레이아웃(부모-자식 또는 확장 가능한 행이라고도 함)은 최상위 목록(마스터)을 통해 사용자가 항목을 확장하여 관련 정보(세부 정보)를 표시할 수 있는 디자인 패턴입니다. 일반적인 예는 다음과 같습니다.

  • 주문 품목(세부 정보)이 있는 주문(마스터)
  • 프로필 로그가 있는 사용자(마스터)(세부 정보)
  • 직원이 있는 부서(마스터)(세부 정보)

이 패턴은 화면 혼란을 줄이는 동시에 필요할 때 더 깊은 데이터에 대한 액세스를 제공하여 유용성을 향상시킵니다.

Angular 프로젝트 설정

마스터-디테일 구현을 시작하기 전에 데모를 위한 깔끔한 Angular 작업 공간을 설정해 보겠습니다.

1. 새 Angular 프로젝트 만들기 

Angular 작업 영역이 아직 준비되지 않은 경우 Angular CLI를 사용하여 작업 영역을 만듭니다. 원하는 IDE 또는 편집기에서 이 프로젝트를 열어 시작합니다.

ng new master-detail-grid 

2. Add Ignite UI for Angular 

Ignite UI for Angular 마스터-디테일 레이아웃에 사용할 igxGrid를 포함하여 강력한 UI 구성 요소를 제공합니다. 추가하는 방법은 다음과 같습니다.

ng add igniteui-angular

3. 전용 데모 구성 요소 생성 

마스터-세부 사항 예제를 모듈식으로 유지하려면 전용 Angular 구성 요소를 생성합니다.

ng generate component pages/grid-demo

4. 데모를 위한 라우팅 구성 

탐색을 단순화하려면 기존 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' } 
];

데이터 설정

마스터-세부 사항 레이아웃을 구축하기 전에 데이터를 준비하는 것이 중요합니다. 이렇게 하면 그리드 구성 요소에 표시할 의미 있는 콘텐츠가 있고 사용자가 UI와 상호 작용할 때 관련 세부 데이터를 효율적으로 가져올 수 있습니다.

다음과 같은 모든 데이터 소스를 사용할 수 Ignite UI for Angular:

  • 정적/로컬 데이터 – 자산 폴더의 JSON 파일에서.
  • 원격 API 데이터 – 백엔드 서비스에서 Angular의 HttpClient를 통해 가져옵니다.
  • 모의 데이터 – 개발을 위해 서비스 자체에서 생성됩니다.

이 예제에서는 사용자 지정 Northwind Swagger API와 통합합니다. 개념은 데이터 소스 구조에 관계없이 동일하게 유지됩니다.

1. Define Your Models 

유형 안전성과 명확성을 적용하려면 전용 models.ts 파일에서 데이터 셰이프를 나타내는 TypeScript 인터페이스를 정의합니다.

export interface Customer { 
  customerId: string; 
  companyName: string; 
  contactName: string; 
  country: string; 
} 
export interface Order { 
  orderId: number; 
  customerId: string; 
  orderDate: string; 
  shipAddress: string; 
  freight: number; 
} 
export interface OrderDetail { 
  orderId: number; 
  productId: number; 
  quantity: number; 
  unitPrice: number; 
}

2. Create a Data Service 

모든 데이터 가져오기 로직을 Angular 서비스(예: northwind-swagger.service.ts)로 중앙 집중화합니다. 다음은 이 예제에서 사용할 사용자 지정 Northwind API와 상호 작용하고 기본 오류 처리를 포함하는 샘플 서비스입니다.

const API_ENDPOINT = 'https://data-northwind.indigo.design'; 

@Injectable({ 

  providedIn: 'root' 

}) 

export class NorthwindSwaggerService { 

  constructor(private http: HttpClient) {} 

  public getCustomerDto(id: string): Observable<Customer | undefined> { 

    return this.http.get<Customer | undefined>(`${API_ENDPOINT}/Customers/${id}`) 

      .pipe(catchError(this.handleError<Customer | undefined>('getCustomerDto', undefined))); 

  } 

  public getCustomerDtoList(): Observable<Customer[]> { 

    return this.http.get<Customer[]>(`${API_ENDPOINT}/Customers`) 

      .pipe(catchError(this.handleError<Customer[]>('getCustomerDtoList', []))); 

  } 

  public getOrderWithDetailsDtoList(id: string): Observable<Order[]> { 

    return this.http.get<Order[]>(`${API_ENDPOINT}/Customers/${id}/Orders/WithDetails`) 

      .pipe(catchError(this.handleError<Order[]>('getOrderWithDetailsDtoList', []))); 

  } 

  private handleError<T>(operation = 'operation', result?: T) { 

    return (error: any): Observable<T> => { 

      console.error(`${operation} failed: ${error.message}`, error); 

      return of(result as T); 

    }; 

  } 

}

모델 및 서비스가 준비되면 그리드 구성 요소는 이제 선택한 고객당 주문 배열에 직접 바인딩할 수 있습니다. 여기에서 마스터 그리드 디자인으로 넘어가고 세부 보기, 사용자 정의 템플릿 및 성능 최적화를 점진적으로 추가하겠습니다.

마스터 그리드 설계

이제 데이터 서비스가 준비되었으므로 마스터-세부 정보 UI를 단계별로 빌드할 차례입니다. 먼저 고객을 선택하고 마스터 그리드에 주문을 표시한 다음 사용자 정의 가능한 세부 템플릿으로 그리드를 향상시킵니다.

1. 고객을 선택하기 위해 콤보 추가 

사용자가 고객을 선택할 수 있도록 서비스에서 고객 데이터를 비동기적으로 로드하는 igx-simple-combo 구성 요소를 사용합니다.

  • 사용자가 고객을 선택하면 구성 요소에서 지역 변수 localCustomerId를 업데이트합니다.
  • 이렇게 하면 해당 고객에 대한 해당 주문을 가져오는 것이 트리거됩니다.
<igx-simple-combo  

        type="border"  

        [data]="northwindSwaggerCustomerDto"  

        displayKey="customerId"  

        (selectionChanging)="localCustomerId = $event.newValue.customerId"  

        class="single-select-combo"> 

    </igx-simple-combo>
public northwindSwaggerCustomerDto: CustomerDto[] = []; 

private _localCustomerId?: string; 

public get localCustomerId(): string | undefined { 

    return this._localCustomerId; 

  } 

  public set localCustomerId(value: string | undefined) { 

    this._localCustomerId = value; 

    this.selectedCustomer$.next(); 

    this.northwindSwaggerOrderWithDetailsDto$.next(); 

  } 

 ngOnInit() { 

this.northwindSwaggerService.getCustomerDtoList().pipe(takeUntil(this.destroy$)).subscribe( 

      data => this.northwindSwaggerCustomerDto = data 

    ); 

}

2. 주문에 대한 마스터 그리드 추가 

고객이 선택되면 igx-grid를 사용하여 주문을 표시합니다. 그리드의 데이터 원본은 northwindSwaggerOrderWithDetailsDto이며, localCustomerId가 변경될 때마다 업데이트됩니다.

<igx-grid [data]="northwindSwaggerOrderWithDetailsDto" primaryKey="orderId" rowSelection="single" [hideRowSelectors]="true" [allowFiltering]="true" filterMode="excelStyleFilter" (rowSelectionChanging)="selectedOrder = $event.newSelection[0]" class="grid"> 

      <igx-column field="orderId" oldDataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

      <igx-column field="customerId" oldDataType="string" header="customerId" [filterable]="true" [sortable]="true" required="true" [minlength]="1" [selectable]="false"></igx-column> 

      <igx-column field="employeeId" oldDataType="number" header="employeeId" [filterable]="true" [sortable]="true" [min]="1" [max]="2147483647" [selectable]="false"></igx-column> 

      <igx-column field="shipperId" oldDataType="number" header="shipperId" [filterable]="true" [sortable]="true" [min]="1" [max]="2147483647" [selectable]="false"></igx-column> 

      <igx-column field="orderDate" oldDataType="date" header="orderDate" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

      <igx-column field="requiredDate" oldDataType="date" header="requiredDate" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

      <igx-column field="shipVia" oldDataType="string" header="shipVia" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

      <igx-column field="freight" oldDataType="number" header="freight" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

      <igx-column field="shipName" oldDataType="string" header="shipName" [filterable]="true" [sortable]="true" [maxlength]="100" [selectable]="false"></igx-column> 

      <igx-column field="completed" oldDataType="boolean" header="completed" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

</igx-grid>
private _selectedCustomer?: CustomerDto; 

  public get selectedCustomer(): CustomerDto | undefined { 

    return this._selectedCustomer; 

  } 

  public set selectedCustomer(value: CustomerDto | undefined) { 

    this._selectedCustomer = value; 

    this.selectedOrder = undefined; 

  } 

  public selectedCustomer$: Subject<void> = new Subject<void>(); 

  public northwindSwaggerOrderWithDetailsDto: OrderWithDetailsDto[] = []; 

  public northwindSwaggerOrderWithDetailsDto$: Subject<void> = new Subject<void>(); 

  public selectedOrder?: OrderWithDetailsDto; 

  ngOnInit() { 

    this.northwindSwaggerService.getOrderWithDetailsDtoList(this.localCustomerId ?? '').pipe(takeUntil(this.destroy$)).subscribe( 

      data => this.northwindSwaggerOrderWithDetailsDto = data 

    ); 

    this.northwindSwaggerOrderWithDetailsDto$.pipe(takeUntil(this.destroy$)).subscribe(() => { 

      this.northwindSwaggerService.getOrderWithDetailsDtoList(this.localCustomerId ?? '').pipe(take(1)).subscribe( 

        data => this.northwindSwaggerOrderWithDetailsDto = data 

      ); 

    }); 

  }

3. 사용자 정의 세부 템플릿 추가 

마스터 그리드를 더 유용하게 만들기 위해 각 주문에 대한 세부 템플릿을 추가합니다. 여기에는 제품 세부 정보, 합계 또는 관련 정보가 포함될 수 있습니다. 우리의 경우 주소를 위한 그리드 열 중 일부를 표시하고 추가로 그리드에 세부 정보를 주문하는 데 사용할 것입니다.

마스터-세부 정보 모드로 표시되도록 igxGrid를 구성하려면 igxGridDetail 지시문으로 표시된 그리드 내부에 템플릿을 지정해야 합니다.

<ng-template igxGridDetail> </ng-template> 

이 세부 정보 템플릿은 비즈니스 요구 사항에 맞는 구성 요소(텍스트, 입력 그룹, 중첩된 그리드 또는 차트)를 사용하여 사용자 지정할 수 있습니다. 여기서는 텍스트와 그리드를 사용합니다.

<ng-template igxGridDetail let-rowData> 

        <div class="row-layout group_3"> 

          <div class="row-layout group_4"> 

            <div class="column-layout group_5"> 

              <p class="ig-typography__subtitle-1 text"> 

                Country 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                Code 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                City 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                Street 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                Phone 

              </p> 

            </div> 

            <div class="column-layout group_6"> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.country }} 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.postalCode }} 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.city }} 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.street }} 

              </p> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.phone }} 

              </p> 

            </div> 

          </div> 

          <div class="column-layout group_7"> 

            <p class="text"> 

              Order Details 

            </p> 

            <igx-grid primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" [data]="rowData.orderDetails" class="grid_1"> 

              <igx-column field="orderId" oldDataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

              <igx-column field="productId" oldDataType="number" header="productId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

              <igx-column field="unitPrice" oldDataType="number" header="unitPrice" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

              <igx-column field="quantity" oldDataType="number" header="quantity" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

              <igx-column field="discount" oldDataType="number" header="discount" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

            </igx-grid> 

          </div> 

        </div> 

      </ng-template>

Handling Data Binding 

템플릿의 컨텍스트는 마스터 레코드 데이터이므로 마스터 레코드의 값을 상세 템플릿에 표시할 수 있습니다.

  • 컨텍스트는 세부 사항이 속한 마스터 레코드입니다.
  • let- 구문을 사용하여 템플릿 컨텍스트 변수를 선언하면 세부 템플릿 내에서 마스터 레코드의 데이터에 액세스할 수 있습니다.
  • 이 예에서는 let-rowData를 사용하여 이 컨텍스트 변수의 이름을 지정합니다.

즉, 세부 템플릿 내에서 rowData를 통해 마스터 레코드의 모든 속성에 액세스할 수 있습니다. 예를 들어 rowData.shipAddress.country 및 rowData.orderDetails는 마스터 그리드의 레코드에서 주문 ID에 액세스합니다.

      <ng-template igxGridDetail let-rowData> 

            <div class="column-layout group_6"> 

              <p class="ig-typography__subtitle-1 text"> 

                {{ rowData.shipAddress.country }} 

              </p> 

          <div class="column-layout group_7"> 

            <igx-grid primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" [data]="rowData.orderDetails" class="grid_1"> 

            </igx-grid> 

          </div> 

        </div> 

      </ng-template>

단순 동적 로딩

대부분의 경우 모든 세부 데이터를 미리 로드하고 싶지 않습니다. 대신 행이 확장될 때 요청 시 가져올 수 있습니다.

세부 데이터가 완전히 다른 엔드포인트에서 제공되며 마스터 레코드의 매개변수(예: 주문 ID)가 필요한 경우가 있습니다. 이러한 경우 행이 확장될 때 필요에 따라 세부 정보를 로드할 수 있습니다.

이 접근 방식에서는 행이 확장될 때마다 마스터 레코드의 ID를 사용하여 API에서 세부 데이터를 요청합니다.

<igx-grid [data]="northwindSwaggerOrderDto" primaryKey="orderId" [allowFiltering]="true" filterMode="excelStyleFilter" class="grid" (rowToggle)="onRowToggle($event)"> 

    <igx-column field="orderId" dataType="number" header="orderId" [filterable]="true" [sortable]="true" [selectable]="false"></igx-column> 

    <ng-template igxGridDetail let-rowData> 

      <ng-container *ngIf="getOrderDetails(rowData.orderId) | async as details"> 

        @for (item of details; track item) { 

          <igx-input-group type="border" class="input"> 

            <input type="text" [(ngModel)]="item.orderId" igxInput /> 

          </igx-input-group> 

        } 

      </ng-container> 

    </ng-template> 

  </igx-grid>
getOrderDetails(orderId: number): Observable<any[]> { 

      return this.northwindSwaggerService 

    .getOrderDetailDtoList(orderId) 

    .pipe( 

      take(1) 

    );   

}

간단하지만 이 접근 방식은 새 API 호출이 표시된 후 지속적으로 트리거합니다. 이로 인해 응용 프로그램 속도가 느려지고 충돌이 발생할 수도 있습니다.

성능 최적화 팁

동일한 세부 데이터를 반복적으로 가져오면 성능과 네트워크 사용량 모두에서 비용이 많이 듭니다. 간단하고 효과적인 솔루션은 데이터를 처음 로드한 후 캐시하는 것입니다.

private orderDetailsCache = new Map<number, Observable<any[]>>(); 

getOrderDetails(orderId: number): Observable<any[]> { 

    if (!this.orderDetailsCache.has(orderId)) { 

      const request$ = this.northwindSwaggerService 

        .getOrderDetailDtoList(orderId) 

        .pipe( 

          take(1), 

          shareReplay(1), 

        ); 

      this.orderDetailsCache.set(orderId, request$); 

    } 

    return this.orderDetailsCache.get(orderId)!; 

  }

어떻게 작동하나요?

  • 행의 첫 번째 확장에서 getOrderDetails()는 API를 호출하고 결과 관찰 가능 항목을 orderDetailsCache에 저장합니다.
  • 이 메서드는 지속적으로 호출되지만 메서드는 캐시된 관찰 가능 항목을 반환하기 때문에 중복 요청을 방지합니다.
  • shareReplay(1) 연산자는 구독자가 API 호출이 완료된 후 연결하더라도 새 요청을 트리거하지 않고 캐시된 데이터를 즉시 수신하도록 합니다.

이는 구현하기 쉽고 요청 수를 크게 줄입니다.

싸다...

Angular에서 마스터-세부 정보 보기를 빌드하는 것은 복잡하거나 성능 집약적일 필요가 없습니다. Ignite UI for Angular를 사용하면 최소한의 설정이 필요하면서도 애플리케이션의 고유한 요구 사항에 맞는 완전한 유연성과 사용자 정의를 제공하는 강력하고 우아한 솔루션을 얻을 수 있습니다.

관리 대시보드, 관리 시스템 또는 데이터 기반 인터페이스를 구축하든 Ignite UI의 Angular 그리드는 빠르고 응답성이 뛰어나며 유지 관리 가능한 애플리케이션을 개발하는 데 필요한 모든 것을 제공합니다.

Ignite UI for Angular 그리드 샘플을 확인하고 사용된 모든 구성 요소와 기능을 탐색할 수 있습니다. 차량 관리 앱은 마스터-세부 정보 그리드의 사용을 보여 주므로 실제 시나리오에서 데이터가 어떻게 표시되고 관리되는지 확인할 수 있습니다.

데모 요청