개요 및 이 글에서 다룰 내용
SAPUI5의 sap.m.IconTabBar는 탭 UI와 데이터 필터링을 하나의 컨트롤로 통합한 컴포지트 컴포넌트입니다. 주문 관리, 티켓 처리, 인보이스 상태 분류 등 상태별 그룹화가 빈번한 업무 화면에서 가장 자주 선택되는 패턴입니다. 이 글에서는 주문 상태(전체/처리중/완료/취소)를 IconTabBar로 전환하면서 List를 동적으로 필터링하는 실전 예제를 단계별로 다룹니다.
핵심 개념
IconTabBar는 단일 컨트롤처럼 보이지만 내부적으로 탭 헤더(IconTabHeader)와 선택된 탭에 매칭되는 콘텐츠 영역 두 가지 역할을 합칩니다. 각 탭은 sap.m.IconTabFilter aggregation으로 정의되며, 사용자가 탭을 누르면 select 이벤트와 함께 해당 탭의 key 값이 전달됩니다.
일반 TabContainer와의 차이를 비유로 설명하면, TabContainer는 "서랍장"입니다. 각 서랍마다 다른 화면이 들어 있고 탭을 누르면 서랍이 바뀝니다. 반면 IconTabBar는 체에 거름종이를 갈아 끼우는 방식에 가깝습니다. 콘텐츠 영역(List)은 그대로이고, 위쪽 탭이 어떤 필터를 적용할지 결정합니다.
IconTabBar는 count 프로퍼티로 탭 헤더에 건수 배지를 자연스럽게 표시할 수 있어 대시보드성 화면과 궁합이 좋습니다. 구조도로 정리하면 다음과 같습니다.
IconTabBar (select 이벤트)
├─ IconTabFilter key="" text="전체"
├─ IconTabFilter key="IN_PROGRESS" text="처리중" count="12"
├─ IconTabFilter key="DONE" text="완료" count="48"
└─ IconTabFilter key="CANCELED" text="취소" count="3"
↓ (콘텐츠 슬롯)
List (items 바인딩 + 동적 Filter)
1단계: XML 선언과 기본 바인딩
주문 상태별 필터링 화면을 만들어봅니다. XML View에서 IconTabBar와 List를 선언합니다.
<mvc:View
controllerName="com.example.order.controller.OrderList"
xmlns:mvc="sap.ui.core.mvc"
xmlns="sap.m">
<Page title="주문 관리">
<IconTabBar
id="statusTabBar"
select="onStatusSelect"
expandable="false">
<items>
<IconTabFilter key="" text="전체" />
<IconTabFilter key="IN_PROGRESS" text="처리중" icon="sap-icon://pending" />
<IconTabFilter key="DONE" text="완료" icon="sap-icon://accept" iconColor="Positive" />
<IconTabFilter key="CANCELED" text="취소" icon="sap-icon://decline" iconColor="Negative" />
</items>
<content>
<List id="orderList" items="{/orders}">
<StandardListItem
title="{orderNo}"
description="{customer}"
info="{status}" />
</List>
</content>
</IconTabBar>
</Page>
</mvc:View>
Controller에서는 select 이벤트를 받아 List 바인딩에 Filter를 적용합니다. "전체" 탭은 key를 빈 문자열로 두는 것이 일반적인 관례입니다.
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator",
"sap/ui/model/json/JSONModel"
], function (Controller, Filter, FilterOperator, JSONModel) {
"use strict";
return Controller.extend("com.example.order.controller.OrderList", {
onInit: function () {
var oModel = new JSONModel({
orders: [
{ orderNo: "SO-1001", customer: "ACME Corp", status: "IN_PROGRESS" },
{ orderNo: "SO-1002", customer: "Globex", status: "DONE" },
{ orderNo: "SO-1003", customer: "Initech", status: "CANCELED" },
{ orderNo: "SO-1004", customer: "Umbrella", status: "IN_PROGRESS" }
]
});
this.getView().setModel(oModel);
},
onStatusSelect: function (oEvent) {
var sKey = oEvent.getParameter("key");
var oBinding = this.byId("orderList").getBinding("items");
var aFilters = sKey ? [new Filter("status", FilterOperator.EQ, sKey)] : [];
oBinding.filter(aFilters);
}
});
});
2단계: select 이벤트와 건수 표시
실무에서는 탭 헤더에 건수(count)를 보여달라는 요구가 빈번합니다. 또한 바인딩이 준비되지 않은 시점에 이벤트가 발화되는 경우를 대비한 방어 로직도 필요합니다.
onInit: function () {
this._refreshCounts();
},
_refreshCounts: function () {
var oData = this.getView().getModel().getProperty("/orders") || [];
var oCounts = oData.reduce(function (acc, item) {
acc[item.status] = (acc[item.status] || 0) + 1;
return acc;
}, {});
var oTabBar = this.byId("statusTabBar");
oTabBar.getItems().forEach(function (oTab) {
var sKey = oTab.getKey();
oTab.setCount(sKey === "" ? String(oData.length) : String(oCounts[sKey] || 0));
});
},
onStatusSelect: function (oEvent) {
var sKey = oEvent.getParameter("key");
var oList = this.byId("orderList");
var oBinding = oList && oList.getBinding("items");
if (!oBinding) {
console.warn("orderList items 바인딩이 준비되지 않았습니다.");
return;
}
var aFilters = sKey
? [new Filter({ path: "status", operator: FilterOperator.EQ, value1: sKey })]
: [];
oBinding.filter(aFilters);
}
setCount에는 문자열을 넘겨야 일부 버전에서 누락되는 문제가 방지됩니다. 모델 데이터가 변경될 때마다 _refreshCounts를 다시 호출해야 count가 갱신됩니다.
3단계: OData 기반 동적 탭과 프로덕션 고려사항
프로덕션 환경에서는 상태 코드가 백엔드에서 관리되므로, 동적으로 탭을 생성해야 새로운 상태가 추가되어도 코드를 수정하지 않아도 됩니다.
_buildStatusTabs: function () {
var that = this;
var oTabBar = this.byId("statusTabBar");
var oModel = this.getOwnerComponent().getModel();
oTabBar.destroyItems();
oTabBar.addItem(new sap.m.IconTabFilter({ key: "", text: "전체" }));
oModel.read("/StatusCodes", {
success: function (oData) {
(oData.results || []).forEach(function (oRow) {
oTabBar.addItem(new sap.m.IconTabFilter({
key: oRow.code,
text: oRow.name,
icon: ({
IN_PROGRESS: "sap-icon://pending",
DONE: "sap-icon://accept",
CANCELED: "sap-icon://decline"
})[oRow.code] || "sap-icon://document",
iconColor: ({
DONE: "Positive",
CANCELED: "Negative",
IN_PROGRESS: "Critical"
})[oRow.code] || "Default"
}));
});
that._refreshCounts();
},
error: function () {
sap.m.MessageBox.error("상태 코드 로드에 실패했습니다.");
}
});
}
프로덕션에서는 다음 사항을 함께 고려하는 것이 권장됩니다.
- 서버 필터: OData v2에서 대용량 데이터는 클라이언트 필터 대신 서버 측 필터로 전환해 페이징과 충돌 방지
- 접근성: text를 i18n 리소스로 관리하고, 아이콘만 있는 탭은 지양
- 보안: 탭 가시성은 UI 단순 토글이므로 실제 권한 분기는 백엔드 인가로 재점검
자주 만나는 실수와 트러블슈팅
select 이벤트가 첫 진입 시 발화되지 않습니다.
IconTabBar는 초기 렌더링 시 select 이벤트를 발화하지 않습니다. 기본 탭의 필터를 즉시 적용해야 한다면 onAfterRendering 또는 라우팅 patternMatched에서 수동 호출하는 것이 일반적입니다.
"전체" 탭을 눌렀는데 빈 리스트가 보입니다.
key가 빈 문자열일 때 new Filter("status", FilterOperator.EQ, "")를 전달하면 status가 빈 값인 항목만 조회됩니다. oBinding.filter([])로 필터를 완전히 제거해야 전체 데이터가 표시됩니다.
count 값이 갱신되지 않습니다.
모델 변경 시 count는 자동 갱신되지 않으므로 데이터 변경 이벤트에서 _refreshCounts를 명시적으로 재호출해야 합니다.
응용 패턴 — SegmentedButton과의 선택 기준
탭이 3개 이하이고 콘텐츠 영역이 동일하다면 sap.m.SegmentedButton이 더 가벼운 대안입니다. 반면 탭이 4개 이상이거나 아이콘/건수 표시가 필요하다면 IconTabBar가 적합합니다. Fiori Elements의 List Report에서는 quickVariantSelection 어노테이션으로 IconTabBar를 자동 생성할 수도 있습니다.
댓글 0
아직 댓글이 없습니다.