개요 및 이 글에서 다루는 것
SAP Fiori Launchpad의 첫인상을 결정하는 것은 단연 타일(Tile)입니다. 그중 sap.m.GenericTile은 헤더, 서브헤더, 본문 콘텐츠를 자유롭게 조합할 수 있는 가장 유연한 컨트롤로, 매출 현황, 주문 건수, KPI 같은 핵심 지표를 한눈에 보여주기 위해 광범위하게 사용됩니다. 이 글에서는 XML 뷰에 GenericTile을 선언하고, TileContent와 NumericContent를 중첩하여 실시간 수치를 표시한 뒤, press 이벤트로 라우팅까지 연결하는 전 과정을 다룹니다.
- GenericTile의 header, subheader, frameType, state 속성 의미 이해
- TileContent 안에 NumericContent를 중첩하여 KPI 형태로 시각화
- press 이벤트 핸들러를 통한 라우팅 또는 외부 URL 이동 처리
- 로딩/실패 상태(state) 표시와 데이터 바인딩 패턴 적용
- 접근성 속성(ariaLabel)과 SizeBehavior 옵션을 활용한 반응형 처리
사전에 알아두면 좋은 내용
이 글은 UI5의 MVC 패턴(XML View, JS Controller)을 한 번이라도 다뤄본 분을 대상으로 합니다. sap.m 네임스페이스 컨트롤(Button, Panel 등)을 XML로 선언해본 경험과 JSONModel 바인딩에 대한 기초 지식이 있으면 진도가 훨씬 빠릅니다. Fiori Launchpad나 BTP Work Zone에서 타일이 어떻게 노출되는지 한 번이라도 본 경험이 있다면 맥락 이해에 도움이 됩니다.
환경 / 버전 / 준비물
예제는 OpenUI5/SAPUI5 1.108 이상 LTS 버전(에디션 무관)에서 동작하도록 작성했습니다. sap.m.GenericTile은 1.20부터 제공되었으며, state="Loading" 같은 상태 모드와 sizeBehavior 옵션은 1.46 이후 안정화되었습니다. 다음과 같은 환경을 권장합니다.
- SAPUI5 1.108 LTS 또는 OpenUI5 1.120 이상
- Node.js 18 이상,
@ui5/cli3.x (로컬 미리보기용) - BAS(Business Application Studio) 또는 VS Code + SAP Fiori Tools 확장
- 샘플 데이터를 위한
manifest.json내 JSONModel 또는 OData V2/V4 목적지 - 크롬/엣지 최신 버전 (개발 도구의 UI5 인스펙터 확장 권장)
본격적인 코드 작성 전에 sap.m 라이브러리가 manifest.json의 sap.ui5.dependencies.libs에 등록되어 있는지 확인합니다. 별도 추가 라이브러리는 필요하지 않습니다.
핵심 개념
GenericTile은 비유하자면 전광판입니다. 외곽 프레임(GenericTile 자체)이 있고, 그 안에 숫자/그래프/이미지가 들어가는 패널(TileContent)이 1개 혹은 2개 자리 잡으며, 패널 내부에 진짜 콘텐츠(NumericContent, ImageContent, FeedContent 등)가 채워지는 3중 구조입니다.
GenericTile (외곽) → TileContent (패널) → NumericContent / FeedContent (콘텐츠)
주요 속성은 다음과 같이 이해하면 됩니다.
- header: 타일 상단 큰 제목. 보통 "월 매출", "오픈 주문" 같은 KPI 이름.
- subheader: header 아래 보조 설명. 기간이나 단위 표기에 자주 쓰임.
- frameType:
OneByOne(정사각) /TwoByOne(가로 2배) /OneByHalf,TwoByHalf(Fiori 3.0 이후) 선택. - state:
Loaded/Loading/Failed/Disabled. 비동기 호출 결과에 따라 자연스러운 UX 제공. - mode:
ContentMode(기본) /HeaderMode(헤더만 표시) /IconMode(아이콘 강조). - press: 클릭 또는 키보드 Enter 시 호출되는 이벤트.
NumericContent는 그 안에서 다시 value(수치), scale(단위/배수), indicator(상승/하락 화살표), valueColor(Good/Critical/Error/Neutral)을 받아 색·아이콘·텍스트를 자동 조합합니다. 즉 개발자는 "데이터만 바인딩"하면 시각 표현은 UI5가 알아서 처리합니다.
다음과 같은 도식으로 정리할 수 있습니다.
<!-- 구조 도식 (실제 코드 아님) -->
GenericTile (header, subheader, press)
└─ TileContent (footer, unit)
└─ NumericContent (value, scale, indicator, valueColor, icon)
실전 코드 3단계
1단계 — 가장 단순한 형태로 선언하기
먼저 XML 뷰에 정적인 GenericTile을 하나 배치합니다. 일별 신규 가입자 수를 보여주는 KPI 카드라고 가정합니다.
<mvc:View
controllerName="kr.acme.dashboard.controller.Home"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc">
<Page title="오늘의 운영 현황">
<HBox justifyContent="Start" wrap="Wrap" renderType="Bare">
<GenericTile
class="sapUiTinyMargin"
header="신규 가입자"
subheader="오늘 00시 기준"
frameType="OneByOne">
<TileContent unit="명" footer="전일 대비">
<NumericContent
value="284"
scale="K"
indicator="Up"
valueColor="Good"
icon="sap-icon://person-placeholder"/>
</TileContent>
</GenericTile>
</HBox>
</Page>
</mvc:View>
이 단계에서는 데이터 바인딩 없이도 타일이 정상 렌더링되는지, 단위(K)와 화살표 인디케이터가 의도대로 출력되는지 확인합니다. scale="K"는 천 단위 약어로, NumericContent가 자동으로 "284K" 형태로 보여줍니다.
2단계 — 모델 바인딩과 상태 전환, 로깅 추가
실무에서는 백엔드에서 받은 수치를 그대로 출력해야 하고, 호출이 늦거나 실패하는 경우도 처리해야 합니다. state 속성을 모델 값에 바인딩하여 로딩/실패 표시를 자동화합니다.
<GenericTile
header="{kpi>/newUsers/title}"
subheader="{kpi>/newUsers/period}"
state="{kpi>/newUsers/state}"
frameType="OneByOne"
press=".onKpiPress">
<customData>
<core:CustomData key="navTarget" value="UserAnalytics" writeToDom="false"/>
</customData>
<TileContent
unit="{kpi>/newUsers/unit}"
footer="{kpi>/newUsers/footer}">
<NumericContent
value="{kpi>/newUsers/value}"
scale="{kpi>/newUsers/scale}"
indicator="{kpi>/newUsers/trend}"
valueColor="{kpi>/newUsers/severity}"
icon="sap-icon://add-employee"/>
</TileContent>
</GenericTile>
컨트롤러에서는 JSONModel을 초기화하면서 일단 Loading 상태로 두고, 비동기 호출이 끝나면 Loaded로 전환합니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/json/JSONModel",
"sap/base/Log"
], function (Controller, JSONModel, Log) {
"use strict";
return Controller.extend("kr.acme.dashboard.controller.Home", {
onInit: function () {
const oKpiModel = new JSONModel({
newUsers: {
title: "신규 가입자",
period: "오늘 00시 기준",
unit: "명",
footer: "전일 대비",
value: 0,
scale: "",
trend: "None",
severity: "Neutral",
state: "Loading"
}
});
this.getView().setModel(oKpiModel, "kpi");
this._loadKpi();
},
_loadKpi: function () {
const oModel = this.getView().getModel("kpi");
// 실제로는 OData/REST 호출. 여기서는 setTimeout으로 흉내.
setTimeout(() => {
try {
oModel.setProperty("/newUsers/value", 1.28);
oModel.setProperty("/newUsers/scale", "K");
oModel.setProperty("/newUsers/trend", "Up");
oModel.setProperty("/newUsers/severity", "Good");
oModel.setProperty("/newUsers/state", "Loaded");
Log.info("KPI 로드 완료", "Home.controller");
} catch (oErr) {
oModel.setProperty("/newUsers/state", "Failed");
Log.error("KPI 로드 실패", oErr, "Home.controller");
}
}, 800);
},
onKpiPress: function (oEvent) {
const oTile = oEvent.getSource();
const sTarget = oTile.data("navTarget");
Log.info(`타일 클릭: ${sTarget}`, "Home.controller");
// 다음 단계에서 라우팅과 연결
}
});
});
여기서 핵심은 customData로 라우팅 타깃을 타일에 직접 주입한 점입니다. 동일한 핸들러를 여러 타일이 공유하면서도 각각 다른 화면으로 이동하게 만드는, 실무에서 매우 자주 쓰는 패턴입니다.
3단계 — 라우팅 연결, 접근성, 테스트까지
마지막으로 press 이벤트에서 Router를 호출해 실제 화면 이동을 구현하고, 접근성과 단위 테스트까지 챙깁니다.
onKpiPress: function (oEvent) {
const oTile = oEvent.getSource();
const sTarget = oTile.data("navTarget");
if (!sTarget) {
sap.m.MessageToast.show("이동 대상이 지정되지 않았습니다.");
return;
}
const oRouter = sap.ui.core.UIComponent.getRouterFor(this);
const oRoute = oRouter.getRoute(sTarget);
if (oRoute) {
oRouter.navTo(sTarget, {}, false);
} else {
// 외부 URL로 폴백 — BTP Launchpad의 인텐트 기반 이동도 동일 패턴
sap.m.URLHelper.redirect(`#${sTarget}`, true);
}
}
XML에는 접근성 라벨과 sizeBehavior를 추가합니다.
<GenericTile
header="신규 가입자"
subheader="오늘 00시 기준"
ariaLabel="신규 가입자 KPI 타일, 클릭 시 상세 분석으로 이동"
sizeBehavior="Responsive"
press=".onKpiPress">
...
</GenericTile>
QUnit으로 press 핸들러가 정상 호출되는지 검증하는 최소한의 테스트도 같이 작성합니다.
sap.ui.define([
"sap/ui/qunit/QUnitUtils",
"sap/m/GenericTile",
"sap/ui/core/CustomData"
], function (QUnitUtils, GenericTile, CustomData) {
"use strict";
QUnit.module("GenericTile press");
QUnit.test("customData의 navTarget을 핸들러로 전달한다", function (assert) {
const done = assert.async();
const oTile = new GenericTile({
header: "테스트",
customData: [new CustomData({ key: "navTarget", value: "UserAnalytics" })],
press: function (oEvt) {
assert.strictEqual(oEvt.getSource().data("navTarget"), "UserAnalytics");
oTile.destroy();
done();
}
});
oTile.placeAt("qunit-fixture");
sap.ui.getCore().applyChanges();
QUnitUtils.triggerEvent("tap", oTile.getDomRef());
});
});
보안 측면에서 한 가지 강조할 점은, press 핸들러에서 받은 값을 절대 그대로 window.location에 넘기지 말고 라우트 이름과 매칭되는 화이트리스트로 검증해야 한다는 것입니다. customData는 DOM에 노출될 수 있으므로 민감 정보(개인 ID, 토큰 등)는 담지 않는 것이 권장됩니다.
흔한 실수 / 트러블슈팅
FAQ 1. 타일을 클릭해도 press가 호출되지 않습니다.
state="Disabled" 또는 state="Failed"일 때는 press가 자동으로 무시됩니다. 모델 바인딩이 의도치 않게 Failed로 떨어졌는지, 또는 부모 컨테이너에 busy="true"가 걸려 있는지 확인합니다.
FAQ 2. NumericContent의 숫자가 잘려서 "..."로 표시됩니다.
frameType="OneByOne"에서 표시 가능한 문자 수가 제한적입니다. scale을 활용해 "1,280,000" 대신 "1.28M"처럼 단축하거나, frameType="TwoByOne"으로 변경해 가로 폭을 확보하면 해결됩니다. truncateValueTo 속성으로 자릿수 한도를 명시할 수도 있습니다.
FAQ 3. valueColor가 의도한 색으로 나오지 않습니다.
valueColor는 자유 색상 코드가 아닌 Good, Critical, Error, Neutral 네 가지 시맨틱 값만 허용합니다. 테마(Quartz Light, Horizon 등)에 따라 실제 RGB가 달라지므로 디자인 검수 시 사용 테마를 함께 확인합니다.
그 외 자주 마주치는 함정으로는, (1) TileContent를 생략하고 NumericContent를 직접 자식으로 두는 경우 렌더링이 깨지는 점, (2) press 이벤트의 oEvent.getSource()가 GenericTile 자신이지만 안쪽 NumericContent에서 발생한 클릭은 버블링된다는 점, (3) BTP Work Zone(구 Launchpad)에서는 자체 타일 카탈로그가 우선이므로 GenericTile을 직접 쓰는 경우는 주로 KPI 워킹 페이지/오버뷰 페이지라는 점이 있습니다.
이어서 살펴볼 주제
GenericTile을 이해했다면 다음 단계로 sap.suite.ui.commons.GenericTile2X2 또는 sap.f.Card로 확장해보길 권장합니다. 특히 Fiori 3.0 이후로는 카드(Card) 기반의 Overview Page와 Manifest 기반 카드가 표준 흐름으로 자리잡고 있어, KPI/리스트/분석 카드를 manifest.json만으로 선언적으로 만드는 패턴을 익히면 활용 폭이 크게 넓어집니다. 추가로 SlideTile(여러 GenericTile을 자동 회전), ActionTile(빠른 액션 버튼) 같은 변형 컨트롤도 살펴볼 가치가 있습니다.
더 읽을거리
댓글 0
아직 댓글이 없습니다.