이 글에서 다룰 것
Fiori Elements List Report는 코드를 거의 쓰지 않고도 목록·필터·내비게이션을 갖춘 앱을 만들어 주지만, "표준 템플릿이라 커스터마이징이 안 된다"는 오해가 여전히 많습니다. 실제로는 freestyle로 새로 짜지 않아도 Annotation, manifest.json, Controller Extension이라는 세 가지 표준 레이어만으로 대부분의 요구사항을 해결할 수 있습니다. 이 글에서는 판매오더(SalesOrder) 관리 앱 시나리오로 그 방법을 단계별로 살펴봅니다.
- List Report 커스터마이징 3계층(Annotation / manifest / Extension)의 역할 구분
- Annotation만으로 컬럼, 필터, 상태 색상(Criticality)을 구성하는 방법
- manifest.json으로 커스텀 액션 버튼과 커스텀 컬럼을 추가하는 방법
- Controller Extension으로 표준 이벤트에 로직을 끼워 넣는 방법
- 프로덕션 관점의 성능 설정과 자주 만나는 함정
이 글을 보기 전에
OData 서비스(V2 또는 V4)의 기본 구조, SAPUI5의 manifest.json 역할, CDS 또는 XML Annotation을 한 번이라도 다뤄본 경험이 있다면 충분합니다. CAP(Cloud Application Programming Model) 기반 예제 코드를 사용하지만, ABAP RAP 기반 서비스에서도 동일한 UI Annotation 개념이 적용되므로 그대로 응용할 수 있습니다.
테스트 환경
이 글의 예제는 아래 환경에서 검증한 구성을 기준으로 합니다. 버전이 달라도 개념은 동일하지만, V2(sap.suite.ui.generic.template)와 V4(sap.fe.templates)는 확장 API가 다르므로 반드시 본인 앱의 템플릿 종류를 먼저 확인하세요.
- SAPUI5 1.120 (LTS) — SAP Fiori elements for OData V4 템플릿 기준
- SAP Business Application Studio (Fiori Dev Space) 또는 VS Code + Fiori Tools
- 백엔드: CAP Node.js 서비스 (RAP 기반 OData V4 서비스도 동일 적용)
- 배포 대상: SAP BTP Cloud Foundry 환경 (HTML5 App Repository)
manifest.json의 "sap.ui5" > "dependencies" > "libs"에 sap.fe.templates가 있으면 V4, sap.suite.ui.generic.template이 있으면 V2 앱입니다.
핵심 개념 — 커스터마이징 3계층
List Report 커스터마이징은 건물 리모델링에 비유하면 이해가 쉽습니다. 벽지를 바꾸는 수준(Annotation), 방 배치를 바꾸는 수준(manifest), 배관을 손대는 수준(Extension)이 있고, 아래층에서 해결될 일을 위층으로 가져가면 유지보수 비용만 늘어납니다.
- 1층 — Annotation:
UI.LineItem(테이블 컬럼),UI.SelectionFields(필터바 필드),UI.DataPoint(상태·수치 표현) 같은 메타데이터로 화면을 선언합니다. 코드가 아니라 데이터 정의이므로 업그레이드 안정성이 가장 높습니다. 커스터마이징 요구의 체감상 70% 이상이 이 층에서 끝납니다. - 2층 — manifest.json: 템플릿 동작 설정(초기 로드 여부, 테이블 타입, 선택 모드)과 확장 포인트 등록(커스텀 액션, 커스텀 컬럼, 커스텀 필터)을 담당합니다. 선언적이라 diff 관리가 쉽고, Fiori Tools의 Page Map으로 GUI 편집도 가능합니다.
- 3층 — Controller Extension: 표준 컨트롤러의 라이프사이클과 이벤트(
onBeforeNavigation,editFlow훅 등)에 JavaScript 로직을 주입합니다. 자유도가 가장 높지만 템플릿 내부 구조에 의존하므로, V4에서는 반드시 공개된 확장 API만 사용하는 것이 권장됩니다.
동작 원리를 한 줄로 요약하면 이렇습니다. 앱 구동 시 템플릿 엔진이 $metadata와 Annotation을 읽어 XML 뷰를 런타임에 생성하고, manifest에 등록된 확장 조각(fragment, controller extension)을 정해진 지점에 병합합니다. 그래서 "화면에 뭔가 추가한다"는 요구는 대부분 코드 작성이 아니라 병합 지점 선언의 문제가 됩니다.
실전 예제 3단계
1단계 — Annotation만으로 화면 구성하기
판매오더 목록에 컬럼 5개, 필터 3개를 배치하고, 오더 상태에 따라 색상을 입혀 보겠습니다. CAP 프로젝트의 app/annotations.cds에 아래처럼 선언합니다.
annotate SalesService.SalesOrders with @(
UI.SelectionFields : [ soNumber, customerName, overallStatus ],
UI.LineItem : [
{ Value : soNumber, Label : '오더번호' },
{ Value : customerName, Label : '고객사' },
{ Value : netAmount, Label : '순금액' },
{ Value : deliveryDate, Label : '납기일' },
{
Value : overallStatus,
Label : '상태',
Criticality : statusCriticality // 1=빨강, 2=노랑, 3=초록
}
]
);
statusCriticality는 백엔드에서 계산해 내려주는 정수 필드입니다. 색상 로직을 프론트에 두지 않고 서비스 계층에서 결정하게 하면, 같은 서비스를 쓰는 다른 앱에서도 일관된 상태 표현을 재사용할 수 있습니다. 저장 후 앱을 새로고침하면 별도 UI 코드 없이 테이블과 필터바가 재구성됩니다.
2단계 — manifest 확장 포인트로 커스텀 액션 추가하기
이제 실무에서 가장 흔한 요구인 "선택한 오더에 대해 승인 요청 버튼 추가"를 구현합니다. V4 템플릿에서는 manifest의 페이지 설정에 액션을 선언하고, 핸들러만 JS로 작성합니다.
{
"sap.ui5": {
"routing": {
"targets": {
"SalesOrdersList": {
"options": {
"settings": {
"initialLoad": "Disabled",
"controlConfiguration": {
"@com.sap.vocabularies.UI.v1.LineItem": {
"actions": {
"requestApproval": {
"press": "salesmgr.ext.OrderActions.onRequestApproval",
"text": "승인 요청",
"requiresSelection": true,
"enabled": "salesmgr.ext.OrderActions.isApprovable"
}
},
"tableSettings": {
"type": "ResponsiveTable",
"selectionMode": "Multi"
}
}
}
}
}
}
}
}
}
}
핸들러 모듈 webapp/ext/OrderActions.js에서는 에러 처리와 로깅을 함께 챙깁니다. 실무에서는 성공 토스트만 띄우고 끝내는 코드가 많은데, 부분 실패(일부 오더만 승인 가능) 케이스를 반드시 다뤄야 합니다.
sap.ui.define([
"sap/m/MessageToast",
"sap/m/MessageBox",
"sap/base/Log"
], function (MessageToast, MessageBox, Log) {
"use strict";
return {
isApprovable: function (oBindingContext, aSelectedContexts) {
return aSelectedContexts && aSelectedContexts.every(
(oCtx) => oCtx.getObject().overallStatus === "N" // 신규 상태만
);
},
onRequestApproval: async function (oBindingContext, aSelectedContexts) {
try {
const aPromises = aSelectedContexts.map((oCtx) =>
this.editFlow.invokeAction("SalesService.requestApproval", {
contexts: oCtx
})
);
await Promise.all(aPromises);
MessageToast.show(aSelectedContexts.length + "건 승인 요청 완료");
} catch (oError) {
Log.error("승인 요청 실패", oError.message, "salesmgr.OrderActions");
MessageBox.error("승인 요청 중 오류가 발생했습니다. 다시 시도해 주세요.");
}
}
};
});
핵심은 this.editFlow.invokeAction입니다. V4 템플릿이 제공하는 공개 API로, OData Action 호출 후 목록 새로고침과 draft 상태 동기화를 템플릿이 알아서 처리해 줍니다. 직접 ODataModel을 호출하면 이 자동 동기화를 잃게 되므로 일반적으로 권장되지 않습니다.
3단계 — Controller Extension과 프로덕션 설정
마지막으로 "특정 권한이 없는 사용자는 상세 페이지 진입 시 경고를 보여준다"는 요구를 Controller Extension으로 구현하고, 성능 설정을 마무리합니다. manifest에 확장을 등록한 뒤,
{
"sap.ui5": {
"extends": {
"extensions": {
"sap.ui.controllerExtensions": {
"sap.fe.templates.ListReport.ListReportController": {
"controllerName": "salesmgr.ext.ListReportExt"
}
}
}
}
}
}
webapp/ext/ListReportExt.js에서 표준 라이프사이클 훅을 오버라이드합니다.
sap.ui.define([
"sap/ui/core/mvc/ControllerExtension"
], function (ControllerExtension) {
"use strict";
return ControllerExtension.extend("salesmgr.ext.ListReportExt", {
override: {
routing: {
onBeforeNavigation: function (mNavigationParameters) {
const oCtx = mNavigationParameters.bindingContext;
if (oCtx.getObject().isRestricted && !this.base._bAcknowledged) {
// 제한 오더는 상세 진입 전 사용자 확인 필요
return false; // 표준 내비게이션 중단 후 커스텀 다이얼로그 처리
}
return true;
}
}
}
});
});
프로덕션 배포 전 체크리스트로는 다음을 권장합니다. 첫째, initialLoad: "Disabled"로 필터 없는 전체 조회를 막아 초기 로딩 부하를 줄입니다. 둘째, 다중 선택 액션에는 tableSettings.selectionLimit을 걸어 대량 호출을 방지합니다. 셋째, 커스텀 액션 핸들러는 OPA5 저니 테스트에 포함시킵니다. Fiori Tools가 생성하는 webapp/test/integration 골격에 버튼 press 시나리오만 추가하면 되므로 비용이 크지 않습니다. 넷째, 확장 코드에서 템플릿 내부 컨트롤을 byId로 직접 집는 패턴은 업그레이드 시 깨질 수 있어 일반적으로 피하는 것이 좋습니다.
자주 만나는 함정
- Q1. Annotation을 바꿨는데 화면에 반영이 안 됩니다. — 가장 흔한 원인은 사용자 변형(Variant)입니다. 사용자가 저장한 테이블 레이아웃이 Annotation보다 우선 적용되므로, 변형을 "표준"으로 초기화한 뒤 확인하세요. 두 번째 원인은 로컬 Annotation 파일과 백엔드 Annotation의 우선순위 충돌입니다. manifest의 dataSources에 선언된 annotation 배열에서 뒤에 오는 파일이 우선합니다.
- Q2. 커스텀 액션 버튼이 아예 안 보입니다. —
controlConfiguration의 키가 정확히@com.sap.vocabularies.UI.v1.LineItem인지 확인하세요. Qualifier가 붙은 LineItem(#Simplified등)을 쓰는 경우 키에도 동일하게 붙여야 합니다. 핸들러 경로 오타는 콘솔에 모듈 로드 에러로 나타나므로 브라우저 개발자 도구를 먼저 확인하는 습관이 좋습니다. - Q3. V2 앱인데 이 글의 코드가 동작하지 않습니다. — V2 템플릿은 확장 구조가 다릅니다. 커스텀 액션은
sap.ui.controllerExtensions아래sap.suite.ui.generic.template.ListReport네임스페이스와 XML fragment 기반viewExtensions를 사용합니다. 개념(3계층)은 같지만 API 이름을 그대로 옮기면 안 됩니다. - Q4. Extension에서
this.getView().byId()가 undefined를 반환합니다. — V4 Controller Extension에서는this.base.getView()로 접근해야 하며, 템플릿이 생성한 컨트롤 ID에는 페이지 prefix가 붙습니다. 애초에 컨트롤 직접 접근 대신editFlow,routing같은 공개 API로 해결할 방법을 먼저 찾는 것이 안전합니다.
더 파볼 주제
이 글은 List Report 한 페이지에 집중했지만, 같은 3계층 원리가 Object Page에도 그대로 적용됩니다. 이어서 보면 좋은 주제는 다음과 같습니다. Object Page의 커스텀 섹션과 Building Block(Flexible Programming Model)으로 freestyle 조각을 템플릿 안에 심는 방법, RAP 기반 서비스에서 Annotation을 메타데이터 확장(Metadata Extension)으로 관리하는 방법, 그리고 Adaptation Project로 배포된 표준 앱을 수정 없이 확장하는 방법입니다. 특히 Building Block은 "표준이냐 freestyle이냐"의 이분법을 없애 주는 방향이라 중급 이상 개발자에게 우선 권장합니다.
더 읽어볼 자료
댓글 0
아직 댓글이 없습니다.