📖 이 글에서 다루는 것
Fiori Elements Object Page는 어노테이션만으로 화면 대부분이 만들어지지만, 실무에서는 표준 CRUD 버튼 외에 업무 고유의 액션 버튼이 반드시 필요해집니다. 이 글은 물류 도메인의 출하 모니터링 앱을 가정하고, Object Page 헤더에 커스텀 액션 버튼을 추가한 뒤 클릭 핸들러에서 백엔드 액션을 호출하는 전체 흐름을 실전 예제로 정리합니다.
- manifest.json의
content/header/actions설정으로 버튼을 선언하는 방법 controllerExtensions로 Object Page 컨트롤러를 확장하는 방법- 핸들러에서
editFlow.invokeAction으로 바운드 액션을 호출하고 에러를 처리하는 방법 - 가시성(visible)·활성화(enabled) 조건을 바인딩 식으로 제어하는 방법
📚 미리 알아두면 좋은 내용
Fiori Elements의 기본 구조(List Report → Object Page 내비게이션)와 CDS 어노테이션 개념, 그리고 JavaScript 모듈 문법(sap.ui.define)을 알고 있으면 수월합니다. OData V4 기준으로 설명하되 V2와의 차이도 짚기 때문에, 두 프로토콜 중 하나라도 다뤄본 경험이 있으면 충분합니다. RAP 액션 구현 자체는 이 글의 범위 밖이며, 백엔드에 액션이 이미 존재한다고 가정합니다.
🔧 환경과 준비물
이 예제는 다음 환경을 기준으로 작성되었습니다.
- SAPUI5 1.120 이상 (Fiori Elements for OData V4,
sap.fe.templates기반) — SAP S/4HANA 2022 이후 또는 SAP BTP 환경에서 일반적으로 사용 가능 - SAP Business Application Studio 또는 VS Code + Fiori Tools 확장
- 백엔드: RAP 기반 OData V4 서비스
ZSD_SHIPMENT_MONITOR, 엔티티ShipmentHeader, 바운드 액션requestExpressCarrier(긴급 배차 요청)가 배포되어 있다고 가정
OData V2(sap.suite.ui.generic.template) 앱이라면 확장 API 이름과 manifest 경로가 다르므로, 본문 중간의 V2 비교 부분을 참고해 치환하면 됩니다. Fiori Tools의 Page Map을 쓰면 아래 manifest 설정 상당 부분을 마법사로 생성할 수 있지만, 생성 결과를 이해하려면 수동 구조를 아는 것이 먼저입니다.
💡 핵심 개념 — 커스텀 액션 버튼이 렌더링되는 원리
Fiori Elements를 "프레임워크가 운전하는 버스"에 비유하면, 커스텀 액션은 정해진 정류장(확장 포인트)에서만 손님을 태우는 것과 같습니다. 아무 데서나 DOM을 조작하는 것이 아니라, 프레임워크가 열어둔 슬롯에 선언적으로 끼워 넣는 방식입니다.
Object Page에 버튼을 추가하는 경로는 크게 두 갈래입니다.
| 방식 | 정의 위치 | 특징 |
|---|---|---|
어노테이션 액션 (@UI.identification 등 + DataFieldForAction) | CDS/메타데이터 | 백엔드 액션과 1:1 매핑, 프론트 코드 불필요. 단, 클릭 시 동작을 프론트에서 세밀하게 제어하기 어려움 |
| manifest 커스텀 액션 | manifest.json + JS 핸들러 | 다이얼로그 표시, 조건 분기, 외부 서비스 호출 등 프론트 로직 자유. 이 글의 주제 |
동작 원리를 단계로 풀면 이렇습니다.
- 앱 기동 시
sap.fe.templates.ObjectPage컴포넌트가 manifest의options/settings를 읽습니다. content → header → actions아래 선언된 항목마다 헤더 액션 바에sap.m.Button이 생성됩니다.press에 지정한 모듈 경로의 함수가 핸들러로 연결되며, 이때 핸들러 내부의this는 ExtensionAPI 인스턴스로 바인딩됩니다. 즉this.editFlow,this.refresh(),this.getModel()같은 안전한 API에 바로 접근할 수 있습니다.visible/enabled에 바인딩 식(expression binding)을 쓰면 편집 모드나 엔티티 상태에 따라 버튼이 동적으로 제어됩니다.
여기서 중요한 설계 원칙 하나 — 버튼 선언은 manifest, 로직은 핸들러 모듈, 화면 생명주기 개입은 controllerExtension으로 역할을 나누는 것이 일반적으로 권장됩니다. 세 가지를 한 파일에 몰아넣으면 업그레이드 시 회귀 범위가 커집니다.
💻 실전 코드 3단계
1단계 — 기본: manifest 선언과 최소 핸들러
먼저 manifest.json의 Object Page 타깃에 커스텀 액션을 선언합니다.
{
"sap.ui5": {
"routing": {
"targets": {
"ShipmentHeaderObjectPage": {
"type": "Component",
"id": "ShipmentHeaderObjectPage",
"name": "sap.fe.templates.ObjectPage",
"options": {
"settings": {
"entitySet": "ShipmentHeader",
"content": {
"header": {
"actions": {
"RequestExpressCarrier": {
"press": "zsd.shipmentmonitor.ext.ShipmentActions.onRequestExpressCarrier",
"text": "{i18n>btnExpressCarrier}",
"visible": true,
"enabled": true
}
}
}
}
}
}
}
}
}
}
}
핸들러 모듈은 webapp/ext/ShipmentActions.js에 만듭니다. 파일 경로와 press의 네임스페이스가 일치해야 합니다.
sap.ui.define(["sap/m/MessageToast"], function (MessageToast) {
"use strict";
return {
onRequestExpressCarrier: function (oEvent) {
// this === ExtensionAPI
var oContext = this.getBindingContext();
MessageToast.show("출하번호 " + oContext.getProperty("ShipmentNumber"));
}
};
});
이 상태로 실행하면 헤더 우측 액션 바에 버튼이 나타나고, 클릭 시 현재 엔티티의 값을 읽어옵니다. 여기까지가 뼈대입니다.
2단계 — 실무: 바운드 액션 호출 + 에러 처리 + 로깅
실무에서는 버튼 클릭이 곧 백엔드 액션 호출입니다. V4에서는 editFlow.invokeAction을 쓰면 트랜잭션 처리와 메시지 핸들링을 프레임워크 흐름에 태울 수 있습니다.
sap.ui.define([
"sap/m/MessageBox",
"sap/base/Log"
], function (MessageBox, Log) {
"use strict";
var LOGGER = Log.getLogger("zsd.shipmentmonitor.ShipmentActions");
return {
onRequestExpressCarrier: function () {
var oExtensionAPI = this;
var oContext = oExtensionAPI.getBindingContext();
MessageBox.confirm("긴급 배차를 요청하시겠습니까? 추가 운임이 발생할 수 있습니다.", {
onClose: function (sAction) {
if (sAction !== MessageBox.Action.OK) { return; }
oExtensionAPI.editFlow
.invokeAction("com.zsd.shipment.requestExpressCarrier", {
contexts: oContext,
parameterValues: [
{ name: "PriorityCode", value: "EXP" }
]
})
.then(function () {
LOGGER.info("Express carrier requested: "
+ oContext.getProperty("ShipmentNumber"));
oExtensionAPI.refresh();
})
.catch(function (oError) {
LOGGER.error("requestExpressCarrier failed", oError);
MessageBox.error("배차 요청이 실패했습니다. 잠시 후 다시 시도하세요.");
});
}
});
}
};
});
포인트 세 가지입니다. 첫째, 백엔드 RAP 액션이 raise한 메시지는 invokeAction 경유 시 메시지 팝오버로 자동 표출되므로 프론트에서 중복 알림을 만들 필요가 없습니다. 둘째, refresh()로 배차 상태 필드를 다시 읽어와야 헤더 표시가 최신화됩니다. 셋째, sap/base/Log에 컴포넌트 전용 로거를 두면 운영 장애 분석 시 필터링이 쉬워집니다.
3단계 — 프로덕션: controllerExtension으로 상태 기반 제어와 테스트 대비
버튼이 "항상 눌리는" 상태라면 아직 프로덕션이 아닙니다. 이미 배차 완료된 출하에서는 비활성화되어야 하고, 초기화 시점 로직도 필요합니다. 이럴 때 manifest의 controllerExtensions로 Object Page 컨트롤러를 확장합니다.
{
"sap.ui5": {
"extends": {
"extensions": {
"sap.ui.controllerExtensions": {
"sap.fe.templates.ObjectPage.ObjectPageController": {
"controllerName": "zsd.shipmentmonitor.ext.controller.ShipmentOPExt"
}
}
}
}
}
}
sap.ui.define([
"sap/ui/core/mvc/ControllerExtension"
], function (ControllerExtension) {
"use strict";
return ControllerExtension.extend("zsd.shipmentmonitor.ext.controller.ShipmentOPExt", {
override: {
onInit: function () {
this.base.getView().setModel(
new sap.ui.model.json.JSONModel({ defaultPriority: "EXP" }), "carrierCfg");
},
routing: {
onAfterBinding: function (oBindingContext) {
}
}
}
});
});
버튼 활성화는 핸들러가 아니라 manifest 바인딩 식으로 선언하는 편이 깔끔합니다. 표시 모드에서만, 그리고 배차 상태가 미완료(CarrierStatus !== 'D')일 때만 활성화하는 예입니다.
"RequestExpressCarrier": {
"press": "zsd.shipmentmonitor.ext.ShipmentActions.onRequestExpressCarrier",
"text": "{i18n>btnExpressCarrier}",
"visible": "{= !${ui>/isEditable} }",
"enabled": "{= %{CarrierStatus} !== 'D' }"
}
보안 관점에서는 프론트 비활성화가 방어선이 아니라는 점을 기억해야 합니다. 실제 차단은 백엔드 RAP의 feature control과 권한 체크가 담당하고, 프론트 enabled는 UX 보조 장치입니다. 테스트는 OPA5 저니에서 iShouldSeeTheButton("긴급 배차 요청") 류의 검증과, 핸들러 모듈을 단독 로드해 invokeAction을 스텁으로 대체하는 QUnit 단위 테스트를 병행하는 구성이 일반적으로 권장됩니다. 참고로 OData V2 앱이라면 같은 목적을 extensionAPI.securedExecution()(busy 처리·데이터 손실 팝업 내장)과 manifest의 Actions 확장 설정으로 달성하며, 확장 API 네임스페이스는 sap.suite.ui.generic.template입니다.
⚠️ 흔한 실수와 트러블슈팅
Q1. 버튼이 아예 안 보입니다.
십중팔구 manifest 경로 문제입니다. content → header → actions 계층이 options/settings 바로 아래에 정확히 위치하는지, 타깃 이름이 라우팅에 실제 등록된 Object Page 타깃과 일치하는지 확인하세요. press 모듈 경로 오타 시에는 버튼은 보이되 클릭 시 콘솔에 모듈 로드 에러가 찍힙니다.
Q2. 핸들러 안에서 this.getView()가 없다고 나옵니다.
V4 커스텀 액션 핸들러의 this는 컨트롤러가 아니라 ExtensionAPI입니다. 뷰 접근이 꼭 필요하면 controllerExtension 쪽에서 처리하거나, ExtensionAPI가 제공하는 getModel/refresh/editFlow 범위 안에서 해결하는 것이 안전합니다. 내부 컨트롤을 ID로 직접 찾는 코드는 업그레이드 시 깨지기 쉬워 지양이 권장됩니다.
Q3. 액션 호출 후 화면 값이 안 바뀝니다.invokeAction 성공 후 this.refresh()를 호출했는지, 또는 백엔드 액션에 side effect 어노테이션이 정의됐는지 확인하세요. 상태 필드만 갱신하려면 side effect 쪽이 네트워크 비용 면에서 유리합니다.
Q4. enabled 바인딩 식이 항상 false입니다.
식 안에서 엔티티 속성은 %{CarrierStatus}(원시값)처럼 참조해야 하며, 필드명이 메타데이터의 대소문자와 정확히 일치해야 합니다. UI 모델 플래그는 ${ui>/isEditable} 경로를 사용합니다.
🚀 이어서 보면 좋은 주제
헤더 액션을 익혔다면 같은 패턴을 테이블 툴바 커스텀 액션(controlConfiguration 아래 @com.sap.vocabularies.UI.v1.LineItem/actions)으로 확장해 보세요. 선택 행 컨텍스트를 받아 일괄 처리하는 패턴이 실무 빈도가 높습니다. 그다음은 커스텀 액션에서 Dialog Fragment를 띄워 파라미터를 입력받는 구성, 그리고 백엔드 관점에서 RAP 액션의 feature control로 버튼 상태를 서버 주도로 제어하는 방법이 자연스러운 순서입니다. 어노테이션 기반 DataFieldForAction과 manifest 커스텀 액션의 선택 기준을 팀 컨벤션으로 정리해 두는 것도 추천합니다.
댓글 0
아직 댓글이 없습니다.