RAP

@UI.facet 없이 Object Page 탭 구성 큰일 #shorts #SAP #RAP

▶ YouTube에서 보기

📖 개요 — 이 글에서 다루는 것

RAP(ABAP RESTful Application Programming Model)으로 Fiori Elements 앱을 처음 만들면 List Report까지는 잘 나오는데, Object Page를 열었더니 화면이 텅 비어 있는 경험을 하게 됩니다. 원인은 대부분 하나입니다. @UI.facet 어노테이션을 정의하지 않았기 때문입니다. Object Page는 facet 계층이 없으면 어떤 섹션도 렌더링하지 않습니다. 이 글에서는 판매 주문(Sales Order) 시나리오를 바탕으로 facet 계층 설계를 단계별로 다룹니다.

  • CollectionFacet과 ReferenceFacet의 역할과 중첩 구조 이해
  • 탭(섹션)과 서브섹션을 parentId로 연결하는 방법
  • Field Group·LineItem을 facet에 바인딩하는 targetQualifier/targetElement 활용
  • facet 누락·중복 position 등 흔한 실수 패턴과 해결책

📚 미리 알아두면 좋은 배경

CDS View Entity 작성 경험과 RAP의 기본 구조(Base View → Projection View → Metadata Extension)를 알고 있으면 수월합니다. Fiori Elements List Report/Object Page 템플릿의 동작 방식, 그리고 @UI.lineItem 같은 필드 단위 UI 어노테이션을 한 번이라도 써봤다면 충분합니다. OData V4 기준으로 설명하지만 V2에서도 개념은 동일합니다.

🔧 환경 및 준비물

이 글의 코드는 다음 환경에서 검증하는 것을 전제로 합니다.

  • SAP BTP ABAP Environment 또는 SAP S/4HANA 2022 이상 (ABAP Platform 2022+, Embedded Steampunk 포함)
  • ABAP Development Tools(ADT) — Metadata Extension 편집과 프리뷰에 필요
  • OData V4 UI Service (Service Binding: OData V4 - UI)
  • 루트 엔터티 SalesOrder와 자식 엔터티 SalesOrderItem으로 구성된 RAP BO (컴포지션 관계 _Item 존재 가정)

일반적으로 UI 어노테이션은 CDS View 본문이 아니라 Metadata Extension(MDE)에 분리해서 작성하는 것이 권장됩니다. 뷰 정의와 UI 레이아웃의 라이프사이클을 분리할 수 있고, 레이어별 오버라이드도 가능해지기 때문입니다.

💡 핵심 개념 — @UI.facet 계층 구조

Object Page를 건물에 비유하면 facet은 설계 도면입니다. @UI.fieldGroup이나 @UI.lineItem으로 아무리 좋은 자재(콘텐츠)를 준비해도, 도면(facet)이 없으면 건물은 지어지지 않습니다. 이것이 "필드 어노테이션은 다 달았는데 Object Page가 비어 있다"는 문제의 본질입니다.

facet은 크게 두 종류로 나뉩니다.

종류type 값역할
CollectionFacet#COLLECTION다른 facet을 묶는 컨테이너. 자체 콘텐츠는 없음
ReferenceFacet#FIELDGROUP_REFERENCE, #LINEITEM_REFERENCE, #IDENTIFICATION_REFERENCEField Group, 테이블 등 실제 콘텐츠를 참조

계층 구조는 다음과 같이 렌더링됩니다.

최상위 facet (parentId 없음) → Object Page의 섹션. 섹션이 여러 개면 앵커 바(탭처럼 보이는 네비게이션)가 생성됨
CollectionFacet의 자식 ReferenceFacet (parentId 지정) → 섹션 내부의 서브섹션
ReferenceFacet의 targetQualifier / targetElement → Field Group 또는 자식 엔터티의 LineItem 테이블에 바인딩

여기서 "탭 vs 섹션" 논쟁이 자주 나오는데, Fiori Elements에서 둘은 별개 어노테이션이 아닙니다. 최상위 facet 하나하나가 섹션이고, 매니페스트 설정(sectionLayout: "Tabs")에 따라 같은 facet 구조가 탭 형태로도, 스크롤형 앵커 섹션으로도 표시됩니다. 즉 CDS에서는 계층만 올바르게 설계하면 되고, 표시 방식은 프런트엔드 설정의 몫입니다. 이 분리를 이해하면 "탭을 만들려고 facet을 억지로 쪼개는" 실수를 피할 수 있습니다.

각 facet에서 자주 쓰는 속성은 id(자식이 참조할 식별자), parentId(부모 CollectionFacet의 id), purpose(#STANDARD는 본문, #HEADER는 헤더 영역), position(정렬 순서), label(섹션 제목)입니다.

💻 실전 코드 — 3단계로 완성하는 SalesOrder Object Page

1단계: 최소 구성 — ReferenceFacet 하나로 화면 살리기

빈 Object Page를 벗어나는 최소 조건은 "Field Group 정의 + 그것을 참조하는 facet" 한 쌍입니다.

@Metadata.layer: #CORE
annotate view ZC_SalesOrder with
{
  @UI.facet: [{
    id:            'GeneralInfoFacet',
    type:          #FIELDGROUP_REFERENCE,
    targetQualifier: 'GeneralInfo',
    label:         '주문 기본 정보',
    position:      10
  }]
  @UI.fieldGroup: [{ qualifier: 'GeneralInfo', position: 10 }]
  SalesOrderId;

  @UI.fieldGroup: [{ qualifier: 'GeneralInfo', position: 20 }]
  CustomerName;

  @UI.fieldGroup: [{ qualifier: 'GeneralInfo', position: 30 }]
  OrderDate;
}

포인트는 targetQualifier: 'GeneralInfo'fieldGroupqualifier정확히 일치해야 한다는 것입니다. 오타가 나도 활성화 에러는 발생하지 않고 그냥 빈 섹션이 되기 때문에 가장 먼저 의심해야 할 지점입니다.

2단계: 실무 시나리오 — CollectionFacet 중첩 + 자식 엔터티 테이블

실제 판매 주문 화면은 "기본 정보 탭(기본 데이터 + 금액 정보 서브섹션)"과 "품목 탭(아이템 테이블)"으로 나누는 것이 일반적입니다. CollectionFacet 안에 ReferenceFacet을 중첩하고, 자식 엔터티는 #LINEITEM_REFERENCE로 연결합니다.

@UI.facet: [
  // ── 섹션 1: 기본 정보 (컨테이너) ──
  { id:       'HeaderCollection',
    type:     #COLLECTION,
    label:    '기본 정보',
    position: 10 },

  // 서브섹션 1-1
  { id:              'GeneralInfoFacet',
    parentId:        'HeaderCollection',
    type:            #FIELDGROUP_REFERENCE,
    targetQualifier: 'GeneralInfo',
    label:           '주문 데이터',
    position:        10 },

  // 서브섹션 1-2
  { id:              'PricingFacet',
    parentId:        'HeaderCollection',
    type:            #FIELDGROUP_REFERENCE,
    targetQualifier: 'Pricing',
    label:           '금액 정보',
    position:        20 },

  // ── 섹션 2: 품목 테이블 ──
  { id:            'ItemsFacet',
    type:          #LINEITEM_REFERENCE,
    label:         '주문 품목',
    position:      20,
    targetElement: '_Item' }
]

targetElement: '_Item'은 컴포지션 어소시에이션 이름이며, 테이블 컬럼은 자식 뷰 ZC_SalesOrderItem@UI.lineItem 어노테이션이 결정합니다.

annotate view ZC_SalesOrderItem with
{
  @UI.lineItem: [{ position: 10 }]
  ItemNumber;
  @UI.lineItem: [{ position: 20 }]
  ProductName;
  @UI.lineItem: [{ position: 30 }]
  Quantity;
  @UI.lineItem: [{ position: 40, criticality: 'PriceCriticality' }]
  NetAmount;
}

디버깅 팁 하나. 섹션이 안 보이면 브라우저 개발자 도구에서 $metadata를 열어 UI.Facets 어노테이션이 실제로 생성됐는지 확인하는 습관을 들이면, CDS 문제인지 캐시 문제인지 빠르게 구분할 수 있습니다.

3단계: 프로덕션 수준 — 헤더 facet, 조건부 표시, 레이어 전략

purpose: #HEADER로 헤더 영역에 요약 정보를 배치하고, hidden에 Boolean 필드를 바인딩해 상태에 따라 섹션을 동적으로 숨길 수 있습니다.

@UI.facet: [
  // 헤더 영역의 데이터포인트 (총액 강조)
  { id:              'TotalAmountHeader',
    purpose:         #HEADER,
    type:            #DATAPOINT_REFERENCE,
    targetQualifier: 'TotalNet',
    position:        10 },

  // 반려 사유 섹션 — 반려 건에서만 표시
  { id:              'RejectionFacet',
    type:            #FIELDGROUP_REFERENCE,
    targetQualifier: 'Rejection',
    label:           '반려 사유',
    position:        30,
    hidden:          #(HideRejectionSection) }
]

HideRejectionSection은 CDS에서 case when OverallStatus = 'REJ' then ... 로 계산하는 abap.boolean 필드입니다. 성능 관점에서는 facet 자체가 쿼리 비용을 만들지는 않지만, hidden 계산 필드가 복잡한 서브쿼리를 타면 Object Page 로딩이 느려지므로 가상 요소(virtual element)나 단순 CASE로 유지하는 것이 좋습니다. 또한 조직 표준으로 @Metadata.layer: #CORE / #CUSTOMER를 구분해 두면, 파트너·고객 레이어에서 position만 바꿔 섹션 순서를 재배치하는 식의 안전한 확장이 가능합니다. 권한 측면에서는 facet으로 숨긴 데이터가 OData 응답에는 여전히 포함된다는 점을 기억해야 합니다. 민감 필드는 UI 숨김이 아니라 DCL(Access Control)로 통제해야 합니다.

⚠️ 흔한 실수와 트러블슈팅

Q1. Object Page가 완전히 비어 있어요.
facet이 하나도 정의되지 않은 경우입니다. @UI.fieldGroup만 달아서는 렌더링되지 않습니다. 최소 1개의 ReferenceFacet과 대상 qualifier 매칭을 확인하세요. MDE 활성화 후에도 안 보이면 브라우저 캐시와 /IWFND/CACHE_CLEANUP(온프레미스 V2의 경우)을 점검합니다.

Q2. 서브섹션이 부모 안으로 안 들어가고 별도 섹션으로 떠요.
parentId 값이 부모 CollectionFacet의 id와 다르거나(대소문자 포함), 부모 type을 #COLLECTION이 아닌 것으로 선언한 경우입니다. parentId가 해석되지 않으면 해당 facet은 최상위로 승격되어 버립니다.

Q3. 품목 테이블 섹션은 보이는데 컬럼이 하나도 없어요.
#LINEITEM_REFERENCEtargetElement는 맞지만, 자식 Projection View에 @UI.lineItem이 없는 경우입니다. facet은 "테이블을 여기에 놓아라"까지만 담당하고 컬럼 구성은 자식 뷰의 책임입니다.

Q4. 섹션 순서가 뒤죽박죽이에요.
position이 중복되면 정렬이 비결정적으로 보일 수 있습니다. 10, 20, 30처럼 간격을 두고 유일하게 부여하고, 나중에 끼워 넣을 여지를 남겨두는 것이 일반적입니다.

🚀 여기서 더 나아가기

facet 계층이 손에 익었다면 다음 주제로 확장해 보세요. 헤더를 풍부하게 만드는 #DATAPOINT_REFERENCE#CHART_REFERENCE, 매니매스트의 sectionLayout: "Tabs"로 앵커 바를 실제 탭 UI로 전환하는 설정, Flexible Programming Model 기반 커스텀 섹션 추가, 그리고 @UI.presentationVariant로 품목 테이블 정렬·집계를 제어하는 방법이 자연스러운 다음 학습 경로입니다. RAP의 Draft 처리와 결합하면 편집 모드에서의 facet 동작 차이도 확인해 볼 가치가 있습니다.

📚 함께 보면 좋은 문서

  • SAP Help Portal — Defining CDS Annotations for Metadata-Driven UIs (RAP)
  • SAP Help Portal — ABAP Data Models: UI Annotations (@UI.facet 레퍼런스)
  • SAP Help Portal — Metadata Extensions
  • SAPUI5 Documentation — Object Page: Facets and Sections

댓글 0

아직 댓글이 없습니다.