1. SmartFilterBar 커스텀 필터가 필요한 이유
SAP UI5의 sap.ui.comp.smartfilterbar.SmartFilterBar는 OData 서비스의 메타데이터($metadata)를 읽어 필터 UI를 자동으로 그려주는 컴포넌트입니다. 엔티티의 속성(Property)에 부여된 sap:filterable, sap:label, sap:value-list 같은 어노테이션만 잘 정의되어 있으면 화면 코드를 거의 작성하지 않고도 검색 화면이 완성되기 때문에 Fiori Elements 시대 이전부터 SAP 표준 화면의 표준 구성요소로 자리 잡았습니다.
하지만 실무 화면은 메타데이터만으로 끝나지 않습니다. "최근 7일 내 등록된 주문만" 같은 동적 기간 필터, 회계연도와 기간을 합친 합성 필드, 외부 시스템에서 가져온 코드값을 표시하는 드롭다운 등 메타데이터에 없는 사용자 경험이 필요합니다. 이때 addFilterGroupItem으로 커스텀 컨트롤을 끼워 넣어야 하는데, 일반 FilterBar처럼 다루다 보면 검색이 동작하지 않거나 변수(Variant) 저장 시 값이 사라지는 문제가 빈번하게 발생합니다.
이 글에서는 중급 개발자가 자주 빠지는 세 가지 함정과 그 해결 패턴을 SalesOrder 검색 화면을 소재로 정리합니다.
학습 체크리스트
- SmartFilterBar가 OData 메타데이터에서 어떤 어노테이션을 읽는지 설명할 수 있다
filterGroupName,groupName,name세 속성의 차이를 구분할 수 있다getFiltersWithValues()와getFilterData()의 동작 차이를 안다- 커스텀 컨트롤의 값을 Variant Management와 연동할 수 있다
2. 미리 알고 있어야 할 내용
이 글은 SAPUI5 1.108 LTS 이상을 기준으로 합니다. sap.ui.comp 라이브러리, OData V2 모델(sap.ui.model.odata.v2.ODataModel), XML View 작성 경험, 그리고 sap.m.MultiInput·DatePicker 같은 기본 컨트롤을 다뤄본 경험이 있다고 가정합니다. Fiori Elements가 아니라 Freestyle UI5 프로젝트에서 SmartFilterBar를 직접 배치하는 시나리오입니다.
3. 환경, 버전, 준비물
실습은 다음 환경에서 검증했습니다.
- SAPUI5 1.120 (LTS, 2025년 기준 권장 버전)
- SAP Business Application Studio 또는 VS Code + Fiori Tools
- OData V2 서비스:
ZSALES_ORDER_SRV(예시), 엔티티SalesOrderSet sap.ui.comp라이브러리 (manifest.json의sap.ui5.dependencies.libs에 추가)- 변수 저장이 필요한 경우
sap.ui.comp.variants.VariantManagement또는sap.ui.comp.smartvariants.SmartVariantManagement
OData V4를 쓴다면 SmartFilterBar 대신 sap.ui.mdc.FilterBar(MDC)를 권장합니다. 이 글의 패턴은 V2 기반 SmartFilterBar에 한정됩니다.
4. 핵심 개념: SmartFilterBar 내부 동작 원리
SmartFilterBar를 이해하는 가장 좋은 방법은 "런타임에 메타데이터를 읽어 자기 자신을 다시 그리는 컨트롤"이라고 보는 것입니다. entitySet 속성을 지정하면 다음 흐름으로 동작합니다.
- 지정된
entitySet의$metadata를 가져옵니다 - 각 Property를 순회하며
sap:filterable="true"인 것만 후보로 모읍니다 com.sap.vocabularies.UI.v1.SelectionFields어노테이션이 있으면 그 순서대로 "기본(Basic)" 그룹에 배치합니다- 나머지는 "More Filters" 다이얼로그에서 추가할 수 있도록 숨겨둡니다
- 각 필드 타입에 맞춰
Input,DateRangeSelection,MultiComboBox같은 컨트롤을 자동 생성합니다
이 과정에서 핵심은 "필터 식별자"가 두 단계로 관리된다는 점입니다. 첫째는 groupName(필드들이 묶이는 시각적 그룹, 예: "주소", "결제"), 둘째는 name(OData Property 이름과 1:1로 매핑되는 식별자)입니다. 커스텀 필터를 추가할 때 이 둘을 혼동하면 필터는 화면에 보이는데 검색에는 반영되지 않는 현상이 발생합니다.
비유하자면 SmartFilterBar는 "조립식 책장"입니다. 메타데이터는 책장의 칸 배치도이고, 자동 생성된 컨트롤은 기본 제공 책입니다. 커스텀 필터는 사용자가 직접 사들고 온 책인데, 어느 칸(groupName)에 어떤 라벨(label)로 꽂을지, 검색 엔진이 이 책을 어떻게 인덱싱(getFiltersWithValues, customData)할지 명시적으로 알려주지 않으면 검색에서 빠지게 됩니다.
5. 실수 1: filterGroupName 누락 문제와 해결
가장 흔한 실수입니다. 커스텀 필터를 추가했는데 검색 버튼을 눌러도 백엔드로 해당 필터값이 전송되지 않는 경우 90% 이상은 이 문제입니다.
잘못된 구현
<smartFilterBar:SmartFilterBar
id="sfbSalesOrder"
entitySet="SalesOrderSet"
persistencyKey="SalesOrderFilterKey">
<smartFilterBar:controlConfiguration>
<!-- 메타데이터 기반 필드 설정 -->
</smartFilterBar:controlConfiguration>
<!-- 커스텀 필터: groupName만 지정, filterGroupName 누락 -->
<smartFilterBar:FilterGroupItem
groupName="_BASIC"
name="OrderAgeBucket"
label="주문 경과 구간">
<smartFilterBar:control>
<Select id="selOrderAge">
<core:Item key="WEEK" text="1주 이내"/>
<core:Item key="MONTH" text="1개월 이내"/>
<core:Item key="QUARTER" text="3개월 이내"/>
</Select>
</smartFilterBar:control>
</smartFilterBar:FilterGroupItem>
</smartFilterBar:SmartFilterBar>
위 코드는 화면에는 셀렉트 박스가 보입니다. 그러나 getFilterData()를 호출해도 OrderAgeBucket 키가 나타나지 않습니다.
올바른 구현
<smartFilterBar:FilterGroupItem
groupName="_BASIC"
groupTitle="기본 검색"
name="OrderAgeBucket"
filterGroupName="CustomAttributes"
label="주문 경과 구간"
visibleInFilterBar="true">
<smartFilterBar:control>
<Select id="selOrderAge" change=".onCustomFilterChange">
<core:Item key="" text="(전체)"/>
<core:Item key="WEEK" text="1주 이내"/>
<core:Item key="MONTH" text="1개월 이내"/>
<core:Item key="QUARTER" text="3개월 이내"/>
</Select>
</smartFilterBar:control>
</smartFilterBar:FilterGroupItem>
filterGroupName은 SmartFilterBar 내부에서 "이 필터는 메타데이터 자동 생성이 아니라 외부에서 들어온 커스텀이다"라고 표시하는 역할을 합니다. 이 속성이 없으면 변수 관리(persistencyKey로 저장되는 사용자 변형)에 포함되지 않고, fireSearch 이벤트의 selectionSet에서도 누락됩니다. 일반적으로 CustomAttributes나 프로젝트별 네임스페이스(com.acme.sales)를 권장합니다.
6. 실수 2: getFiltersWithValues 미구현과 해결
두 번째 실수는 검색 이벤트 핸들러에서 OData 필터를 만들 때 발생합니다. 메타데이터 기반 필드는 SmartFilterBar가 알아서 sap.ui.model.Filter로 변환해 주지만, 커스텀 필터는 개발자가 직접 변환해야 합니다.
잘못된 구현
onSearch: function (oEvent) {
var oTable = this.byId("tblSalesOrder");
var oBinding = oTable.getBinding("items");
// 커스텀 필터를 잊고 selectionSet만 그대로 사용
var aFilters = oEvent.getParameter("selectionSet");
oBinding.filter(aFilters);
}
selectionSet은 "화면에 보이는 컨트롤 인스턴스 배열"일 뿐 OData Filter가 아닙니다. 또한 커스텀 컨트롤은 자체적으로 filter 객체를 만들지 않습니다.
올바른 구현
sap.ui.define([
"sap/ui/core/mvc/Controller",
"sap/ui/model/Filter",
"sap/ui/model/FilterOperator",
"sap/base/Log"
], function (Controller, Filter, FilterOperator, Log) {
"use strict";
return Controller.extend("com.acme.sales.controller.SalesOrderList", {
onSearch: function (oEvent) {
var oSfb = this.byId("sfbSalesOrder");
var oTable = this.byId("tblSalesOrder");
var oBinding = oTable.getBinding("items");
// 1. 메타데이터 기반 필터 (SmartFilterBar가 자동 생성)
var aMetaFilters = oSfb.getFilters();
// 2. 값이 입력된 필드만 골라내기 (커스텀 포함)
var aActiveItems = oSfb.getFiltersWithValues();
var aCustomFilters = [];
aActiveItems.forEach(function (oItem) {
if (oItem.getFilterGroupName() !== "CustomAttributes") {
return; // 메타데이터 기반은 이미 aMetaFilters에 포함
}
var oControl = oItem.getControl();
var sValue = oControl.getSelectedKey
? oControl.getSelectedKey()
: oControl.getValue();
if (!sValue) {
return;
}
var oFilter = this._buildCustomFilter(oItem.getName(), sValue);
if (oFilter) {
aCustomFilters.push(oFilter);
}
}.bind(this));
var aAll = aMetaFilters.concat(aCustomFilters);
Log.info("[SalesOrder] 적용된 필터 수: " + aAll.length, null, "SalesOrderList");
try {
oBinding.filter(aAll);
} catch (e) {
Log.error("필터 적용 실패: " + e.message);
sap.m.MessageToast.show("검색 조건을 확인해 주세요.");
}
},
_buildCustomFilter: function (sName, sValue) {
if (sName === "OrderAgeBucket") {
var oNow = new Date();
var oFrom = new Date(oNow);
if (sValue === "WEEK") oFrom.setDate(oNow.getDate() - 7);
if (sValue === "MONTH") oFrom.setMonth(oNow.getMonth() - 1);
if (sValue === "QUARTER") oFrom.setMonth(oNow.getMonth() - 3);
return new Filter("CreatedAt", FilterOperator.BT, oFrom, oNow);
}
return null;
}
});
});
getFiltersWithValues()는 값이 채워진 FilterGroupItem만 반환하기 때문에, 전체 filterGroupItems를 순회하며 빈 값을 거르는 코드를 직접 작성하는 것보다 안전하고 빠릅니다.
7. 실수 3: customData 바인딩 누락과 해결
세 번째는 변수 관리(Variant)와 연관된 문제입니다. 사용자가 자주 쓰는 검색 조건을 변형으로 저장하고 다시 불러올 때, 커스텀 필터는 기본적으로 저장 대상이 아닙니다. 이를 풀려면 customData를 통해 "이 컨트롤의 어떤 속성을 직렬화할지" 알려줘야 합니다.
해결 코드
<smartFilterBar:FilterGroupItem
groupName="_BASIC"
name="OrderAgeBucket"
filterGroupName="CustomAttributes"
label="주문 경과 구간"
visibleInFilterBar="true">
<smartFilterBar:control>
<Select id="selOrderAge">
<customData>
<core:CustomData
key="sap.ui.comp.smartfilterbar.SmartFilterBar.CUSTOM_VALUE"
value="{path: '/OrderAgeBucket', model: 'customFilters'}"
writeToDom="false"/>
</customData>
<core:Item key="" text="(전체)"/>
<core:Item key="WEEK" text="1주 이내"/>
<core:Item key="MONTH" text="1개월 이내"/>
<core:Item key="QUARTER" text="3개월 이내"/>
</Select>
</smartFilterBar:control>
</smartFilterBar:FilterGroupItem>
컨트롤러에서는 별도의 JSON 모델 customFilters를 두고, filterChange 이벤트에서 값을 동기화합니다. 이렇게 하면 SmartVariantManagement가 모델 데이터를 읽어 변형에 포함시킵니다.
onInit: function () {
var oCustomModel = new sap.ui.model.json.JSONModel({
OrderAgeBucket: ""
});
this.getView().setModel(oCustomModel, "customFilters");
},
onCustomFilterChange: function (oEvent) {
var oSelect = oEvent.getSource();
this.getView().getModel("customFilters")
.setProperty("/OrderAgeBucket", oSelect.getSelectedKey());
}
8. 실전 예제: SalesOrder 검색 화면 통합
지금까지의 세 가지 패턴을 합쳐 "고객 코드 + 상태 + 주문 경과 구간"으로 검색하는 화면을 만들어 봅니다. CustomerCode와 OrderStatus는 메타데이터 자동 생성, OrderAgeBucket은 커스텀입니다.
<mvc:View
controllerName="com.acme.sales.controller.SalesOrderList"
xmlns="sap.m"
xmlns:mvc="sap.ui.core.mvc"
xmlns:core="sap.ui.core"
xmlns:smartFilterBar="sap.ui.comp.smartfilterbar"
xmlns:smartTable="sap.ui.comp.smarttable">
<Page title="주문 검색">
<smartFilterBar:SmartFilterBar
id="sfbSalesOrder"
entitySet="SalesOrderSet"
persistencyKey="ZSALES_FILTER_V1"
enableBasicSearch="false"
search=".onSearch"
filterChange=".onFilterBarChange">
<smartFilterBar:controlConfiguration>
<smartFilterBar:ControlConfiguration
key="CustomerCode"
groupId="_BASIC"
visibleInAdvancedArea="true"/>
<smartFilterBar:ControlConfiguration
key="OrderStatus"
groupId="_BASIC"
visibleInAdvancedArea="true"/>
</smartFilterBar:controlConfiguration>
<smartFilterBar:FilterGroupItem
groupName="_BASIC"
name="OrderAgeBucket"
filterGroupName="CustomAttributes"
label="주문 경과 구간"
visibleInFilterBar="true">
<smartFilterBar:control>
<Select selectedKey="{customFilters>/OrderAgeBucket}"
change=".onCustomFilterChange">
<core:Item key="" text="(전체)"/>
<core:Item key="WEEK" text="1주 이내"/>
<core:Item key="MONTH" text="1개월 이내"/>
<core:Item key="QUARTER" text="3개월 이내"/>
</Select>
</smartFilterBar:control>
</smartFilterBar:FilterGroupItem>
</smartFilterBar:SmartFilterBar>
<smartTable:SmartTable
id="tblSalesOrder"
entitySet="SalesOrderSet"
smartFilterId="sfbSalesOrder"
tableType="ResponsiveTable"
useExportToExcel="true"/>
</Page>
</mvc:View>
SmartTable의 smartFilterId로 두 컨트롤을 연결하면 검색 버튼이 자동으로 테이블 바인딩을 갱신합니다. 단, 커스텀 필터까지 적용하려면 위에서 만든 onSearch 핸들러에서 preventDefault 없이 oBinding.filter(aAll)을 명시적으로 호출해야 합니다.
커스텀 필터와 OData 파라미터 연동
때로는 $filter가 아니라 OData function import의 파라미터로 보내야 할 때가 있습니다. 이 경우 oBinding.filter 대신 oModel.callFunction()을 호출하면서 커스텀 값을 파라미터로 매핑합니다.
var oModel = this.getOwnerComponent().getModel();
oModel.callFunction("/GetSalesOrdersByAge", {
method: "GET",
urlParameters: {
AgeBucket: this.getView().getModel("customFilters").getProperty("/OrderAgeBucket"),
CustomerCode: this._readSfbValue("CustomerCode")
},
success: function (oData) { /* 테이블에 바인딩 */ },
error: function (oErr) { sap.m.MessageBox.error(oErr.message); }
});
흔한 실수와 FAQ
Q1. 검색은 되는데 변수에 저장이 안 됩니다. filterGroupName이 __BASIC(앞에 언더스코어 2개)으로 잘못 설정된 경우가 많습니다. 표준 그룹과 충돌하지 않는 임의 이름(예: CustomAttributes)을 쓰세요.
Q2. 변수에서 복원 시 셀렉트가 비어 있습니다. customData의 writeToDom이 true이면 DOM에 직렬화되어 성능 저하가 발생하고, 키 이름이 정확히 sap.ui.comp.smartfilterbar.SmartFilterBar.CUSTOM_VALUE가 아니면 인식되지 않습니다. 키 문자열을 그대로 복사하세요.
Q3. getFiltersWithValues가 빈 배열을 반환합니다. 커스텀 컨트롤의 값 변경 이벤트에서 모델 업데이트만 하고 fireFilterChange를 호출하지 않으면 SmartFilterBar는 값이 채워진 줄 모릅니다. this.byId("sfbSalesOrder").fireFilterChange();를 명시적으로 호출하거나, 양방향 바인딩을 통해 자동 갱신되도록 만드세요.
Q4. "More Filters" 다이얼로그에서 커스텀 필드가 사라집니다. visibleInFilterBar와 visible은 다릅니다. 전자는 기본 화면 노출 여부, 후자는 다이얼로그 포함 여부입니다. 둘 다 true여야 항상 보입니다.
이후 더 살펴볼 주제
OData V4 환경이라면 sap.ui.mdc.FilterBar와 FilterField로 넘어가는 것이 일반적으로 권장됩니다. MDC는 메타데이터 드라이븐 컨트롤(Metadata Driven Controls)의 차세대 표준으로, ValueHelp·Delegate 패턴을 통해 커스텀 필터를 훨씬 깔끔하게 끼워 넣을 수 있습니다. 또한 Fiori Elements V4의 SelectionFields 어노테이션과 FilterRestrictions를 함께 학습하면 백엔드 CAP/RAP 모델과의 일관성을 유지할 수 있습니다. 변수 관리 UX를 강화하려면 SmartVariantManagement의 StandardVariant 처리와 즐겨찾기/공유 기능도 다음 학습 대상입니다.
참고할 만한 문서
- SAPUI5 API: sap.ui.comp.smartfilterbar.SmartFilterBar
- SAPUI5 API: FilterGroupItem (filterGroupName 속성)
- Demo Kit: Adding Custom Fields to the SmartFilterBar
- help.sap.com: SAP Fiori Elements Documentation
- help.sap.com: SmartFilterBar Annotation Reference
- help.sap.com: SAPUI5 Flexibility & Variant Management
- Demo Kit: Using SmartFilterBar with OData V2
댓글 0
아직 댓글이 없습니다.